fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed

- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner
- P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var
- P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup
- P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns
- P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs)
- P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import
- P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default
- P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed
- P2: 28 frontend TODOs (hardcoded constants, deprecated notification API)
- P3: dead code, duplicates, deprecated imports, private attr, __import__ inline
- P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n)
- ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix)
- F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String)
- Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
This commit is contained in:
Agent Zero
2026-08-16 01:17:18 +02:00
parent 3d9b76cea4
commit abbe7a18fc
306 changed files with 5912 additions and 1827 deletions
+49 -137
View File
@@ -16,36 +16,34 @@ Resolution, caching, and audit logic have been extracted into focused modules:
from __future__ import annotations
import json
import logging
import uuid
from datetime import datetime, UTC
from datetime import UTC, datetime
from typing import Any
import redis.asyncio as aioredis
from sqlalchemy import and_, func, or_, select, text
from sqlalchemy import String, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.audit import log_audit
from app.core.notifications import create_notification
from app.models.entity_permission import EntityPermission
from app.models.group import Group, UserGroup
from app.models.role import Role
from app.models.user import User, UserTenant
from app.models.contact import Contact
from app.core.notifications import post_system_message
from app.models.address import Address
from app.models.attachment import Attachment
from app.models.bank_account import BankAccount
from app.models.workflow import Workflow
from app.models.sequence import Sequence
from app.models.contact import Contact
from app.models.contact_folder import ContactFolder
from app.models.custom_field_definition import CustomFieldDefinition
from app.models.entity_permission import EntityPermission
from app.models.group import Group, UserGroup
from app.models.role import Role
from app.models.saved_filter import SavedFilter
from app.models.saved_view import SavedView
from app.models.sequence import Sequence
from app.models.user import User
from app.models.webhook import Webhook
from app.models.custom_field_definition import CustomFieldDefinition
from app.models.contact_folder import ContactFolder
from app.models.workflow import Workflow
# Import cache helpers used by CRUD operations
from app.services.permission_cache import _invalidate_user_cache, CACHE_TTL, CACHE_PREFIX
from app.services.permission_cache import _invalidate_user_cache
logger = logging.getLogger(__name__)
@@ -54,6 +52,8 @@ logger = logging.getLogger(__name__)
# This replaces insecure text(f"SELECT ... FROM {entity_type}s") queries
# with safe SQLAlchemy model-based queries (prevents SQL injection).
ENTITY_MODELS: dict[str, type] = {
# Core models only — plugin models are registered dynamically
# via plugin.get_entity_models() at activation time (P0-3 fix).
"contact": Contact,
"contacts": Contact,
"company": Contact,
@@ -81,113 +81,18 @@ try:
except ImportError:
pass
# Plugin models if available
try:
from app.plugins.builtins.dms.models import File as DmsFile
ENTITY_MODELS["file"] = DmsFile
except ImportError:
pass
try:
from app.plugins.builtins.dms.models import Folder as DmsFolder
ENTITY_MODELS["folder"] = DmsFolder
except ImportError:
pass
try:
from app.plugins.builtins.calendar.models import CalendarEntry
ENTITY_MODELS["calendar_event"] = CalendarEntry
except ImportError:
pass
try:
from app.plugins.builtins.calendar.models import Calendar
ENTITY_MODELS["calendar"] = Calendar
except ImportError:
pass
try:
from app.plugins.builtins.calendar.models import Subtask
ENTITY_MODELS["subtask"] = Subtask
except ImportError:
pass
try:
from app.plugins.builtins.tasks.models import Task
ENTITY_MODELS["task"] = Task
except ImportError:
pass
try:
from app.plugins.builtins.mail.models import MailAccount
ENTITY_MODELS["mailbox"] = MailAccount
ENTITY_MODELS["mail_account"] = MailAccount
except ImportError:
pass
# Plugin models are registered dynamically via plugin.get_entity_models()
# at activation time in main.py:lifespan(). No hardcoded plugin imports here.
# Additional plugin models with OwnedMixin
try:
from app.plugins.builtins.mail.models import MailMessage
ENTITY_MODELS["mail_message"] = MailMessage
except ImportError:
pass
try:
from app.plugins.builtins.kommunikation.models import CommConversation
ENTITY_MODELS["comm_conversation"] = CommConversation
except ImportError:
pass
try:
from app.plugins.builtins.tags.models import Tag
ENTITY_MODELS["tag"] = Tag
except ImportError:
pass
try:
from app.plugins.builtins.agent_memory.models import AgentMemory
ENTITY_MODELS["agent_memory"] = AgentMemory
except ImportError:
pass
try:
from app.plugins.builtins.graph_rag.models import EntityRelationship
ENTITY_MODELS["entity_relationship"] = EntityRelationship
except ImportError:
pass
try:
from app.plugins.builtins.report_generator.models import ReportTemplate, ReportInstance
ENTITY_MODELS["report_template"] = ReportTemplate
ENTITY_MODELS["report_instance"] = ReportInstance
except ImportError:
pass
try:
from app.plugins.builtins.entity_links.models import EntityLink
ENTITY_MODELS["entity_link"] = EntityLink
except ImportError:
pass
try:
from app.plugins.builtins.kommunikation.models import CommConversation as CommConv
ENTITY_MODELS["comm_conversation"] = CommConv
except ImportError:
pass
try:
from app.plugins.builtins.ai_proactive.models import ProactiveSuggestion
ENTITY_MODELS["proactive_suggestion"] = ProactiveSuggestion
except ImportError:
pass
try:
from app.plugins.builtins.ai_assistant.models import AIAgent, AIChatSession
ENTITY_MODELS["ai_agent"] = AIAgent
ENTITY_MODELS["ai_chat_session"] = AIChatSession
except ImportError:
pass
try:
from app.plugins.builtins.permissions.models import ShareLink
ENTITY_MODELS["share_link"] = ShareLink
except ImportError:
pass
try:
from app.plugins.builtins.automation.models import AgentDefinition, AutomationDefinition
ENTITY_MODELS["agent_definition"] = AgentDefinition
ENTITY_MODELS["automation_definition"] = AutomationDefinition
except ImportError:
pass
try:
from app.plugins.builtins.mcp_client.models import McpServerConfig
ENTITY_MODELS["mcp_server_config"] = McpServerConfig
except ImportError:
pass
def register_entity_model(entity_type: str, model_class: type) -> None:
"""Register an entity model dynamically (called during plugin activation)."""
ENTITY_MODELS[entity_type] = model_class
def unregister_entity_model(entity_type: str) -> None:
"""Unregister an entity model (called during plugin deactivation)."""
ENTITY_MODELS.pop(entity_type, None)
def _get_entity_model(entity_type: str) -> type:
@@ -197,7 +102,7 @@ def _get_entity_model(entity_type: str) -> type:
raise ValueError(f"Unknown entity type: {entity_type}")
return model
from app.core.permissions import PERM_RANK as _PERM_RANK
from app.core.permissions import PERM_RANK as _PERM_RANK # noqa: E402
def _rank(level: str) -> int:
@@ -305,9 +210,9 @@ async def create_permission(
)
# Notify user if direct permission
if principal_type == 'user':
await create_notification(
await post_system_message(
db, tenant_id, principal_uuid,
type='permission_granted',
message_type='permission_granted',
title='Neue Berechtigung',
body=f'{entity_type} wurde mit dir geteilt',
entity_type=entity_type,
@@ -363,9 +268,9 @@ async def create_permission(
)
# Notify user if direct permission
if principal_type == 'user':
await create_notification(
await post_system_message(
db, tenant_id, principal_uuid,
type='permission_granted',
message_type='permission_granted',
title='Neue Berechtigung',
body=f'{entity_type} wurde mit dir geteilt',
entity_type=entity_type,
@@ -460,9 +365,9 @@ async def delete_permission(
)
# Notify user if direct permission
if old_principal_type == 'user':
await create_notification(
await post_system_message(
db, tenant_id, old_principal_id,
type='permission_revoked',
message_type='permission_revoked',
title='Berechtigung entfernt',
body=f'{old_entity_type} wurde nicht mehr mit dir geteilt',
entity_type=old_entity_type,
@@ -592,15 +497,22 @@ async def cleanup_expired_permissions(db: AsyncSession) -> int:
logger.info("Cleaned up %d expired entity permissions", count)
return count
# Backward compatibility re-exports
from app.services.permission_resolver import ( # noqa: E402
get_effective_access,
get_visible_ids,
batch_get_effective_access,
check_entity_access,
# Backward compatibility re-exports (intentional re-exports used by other modules)
from app.services.permission_cache import ( # noqa: E402
get_cached_visible_ids as get_cached_visible_ids,
)
from app.services.permission_cache import ( # noqa: E402
get_cached_visible_ids,
invalidate_all_user_entity_cache,
invalidate_all_user_entity_cache as invalidate_all_user_entity_cache,
)
from app.services.permission_resolver import ( # noqa: E402
batch_get_effective_access as batch_get_effective_access,
)
from app.services.permission_resolver import ( # noqa: E402
check_entity_access as check_entity_access,
)
from app.services.permission_resolver import ( # noqa: E402
get_effective_access as get_effective_access,
)
from app.services.permission_resolver import ( # noqa: E402
get_visible_ids as get_visible_ids,
)