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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user