fix(security): 16 mittlere Probleme behoben (P18-P33)
Check Cross-Plugin Imports / check (push) Has been cancelled

P18: require_permission zu forgejo_error_reporter und ai_ui_control routes hinzugefügt
P19: Cross-Tenant Permission-Cache-Invalidierung bei Rollenänderungen
P20: Session/Permission-Cache-Invalidierung bei Gruppen-Änderungen
P21: ENTITY_MODELS Registry um fehlende Plugin-Modelle erweitert
P22: Entity-Links prüfen verknüpfte Entity-Permissions
P23: authStore persist Middleware entfernt (kein localStorage mehr)
P24: 5xx Retry nur noch für GET-Requests
P25: KI-Kommentar in address.py (bekannte Inkonsistenz)
P26: DeletionLog in EntityHistory gemerged (action=delete)
P27: KI-Kommentar in entity_policy.py (ABAC nicht aktiv genutzt)
P28: db.commit() aus bulk_permission_service entfernt
P29: CSV-Export in export_service.py ausgelagert
P30: plugins.py Business-Logik in plugin_install_service.py ausgelagert
P31: KI-Kommentar in session.py (Dual-System dokumentiert)
P32: Migration 0115: crm_platform_admin Role droppen
P33: Cross-Plugin Imports über contracts.py behoben (10 Violations → 0)
This commit is contained in:
Agent Zero
2026-08-06 13:23:58 +02:00
parent 9f79107fa7
commit 0eb6d7621e
32 changed files with 593 additions and 307 deletions
+1 -1
View File
@@ -151,7 +151,7 @@ async def update_memory(
if "content" in data:
memory.content = data["content"]
# Regenerate embedding for updated content
from app.plugins.builtins.unified_search.embedding import generate_embedding
from app.plugins.builtins.unified_search.contracts import generate_embedding
embedding = await generate_embedding(data["content"], db=db, tenant_id=tenant_id)
if embedding:
from sqlalchemy import text as sql_text
@@ -9,7 +9,7 @@ from sqlalchemy import select, text as sql_text
from sqlalchemy.ext.asyncio import AsyncSession
from app.plugins.builtins.agent_memory.models import AgentMemory
from app.plugins.builtins.unified_search.embedding import generate_embedding
from app.plugins.builtins.unified_search.contracts import generate_embedding
async def store_memory(
@@ -183,14 +183,14 @@ async def get_agent_status_external(
raise HTTPException(status_code=404, detail="Agent not found")
# Get recent run stats
from app.plugins.builtins.automation.models import AgentRun
from app.plugins.builtins.automation.contracts import AutomationContract
from sqlalchemy import func
recent_runs = await db.execute(
select(func.count())
.select_from(AgentRun)
.where(AgentRun.agent_id == aid)
.where(AgentRun.tenant_id == tenant_id)
.select_from(AutomationContract.AgentRun)
.where(AutomationContract.AgentRun.agent_id == aid)
.where(AutomationContract.AgentRun.tenant_id == tenant_id)
)
total_runs = recent_runs.scalar() or 0
+3 -2
View File
@@ -14,6 +14,7 @@ import uuid
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from app.deps import require_permission
from app.plugins.builtins.ai_ui_control.schemas import (
UICommand,
UICommandCreate,
@@ -31,7 +32,7 @@ router = APIRouter(prefix="/api/v1/ai-ui-control", tags=["ai-ui-control"])
# ─── REST endpoints (for AI agents) ───
@router.post("/command", response_model=UICommandResponse)
@router.post("/command", response_model=UICommandResponse, dependencies=[Depends(require_permission("ai_ui_control:write"))])
async def send_ui_command(
request: Request,
body: UICommandCreate,
@@ -168,7 +169,7 @@ async def get_command_status(
)
@router.get("/online-users")
@router.get("/online-users", dependencies=[Depends(require_permission("ai_ui_control:read"))])
async def get_online_users(request: Request):
"""Check which users are currently online (have active frontend WS connections)."""
from app.config import get_settings
@@ -263,7 +263,8 @@ class AgentCoordinator:
def register_agent_coordinator_tools():
"""Register AgentCoordinator tools in the global tool registry."""
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
from app.plugins.builtins.ai_assistant.contracts import AIAssistantContract
get_tool_registry = AIAssistantContract.get_tool_registry
registry = get_tool_registry()
@@ -355,7 +356,8 @@ def register_agent_coordinator_tools():
def unregister_agent_coordinator_tools():
"""Unregister AgentCoordinator tools."""
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
from app.plugins.builtins.ai_assistant.contracts import AIAssistantContract
get_tool_registry = AIAssistantContract.get_tool_registry
registry = get_tool_registry()
registry.unregister("create_subtask")
@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.core.visibility import apply_visibility_filter
from app.deps import get_current_user, require_permission
from app.services.entity_permission_service import check_entity_access
from app.plugins.builtins.entity_links.models import EntityLink
from app.plugins.builtins.entity_links.schemas import EntityLinkRequest
@@ -47,6 +48,14 @@ async def link_file_to_entity(
400, detail={"detail": "Invalid entity_type", "code": "invalid_entity_type"}
)
# Verify user has read access to both the file and the target entity
has_file_access = await check_entity_access(db, tenant_id, user_id, "file", fid, "read")
if not has_file_access:
raise HTTPException(403, detail={"detail": "No access to file", "code": "forbidden"})
has_entity_access = await check_entity_access(db, tenant_id, user_id, body.entity_type, entity_id, "read")
if not has_entity_access:
raise HTTPException(403, detail={"detail": "No access to target entity", "code": "forbidden"})
# Check if link already exists
existing = await db.execute(
select(EntityLink).where(
@@ -4,14 +4,16 @@ from __future__ import annotations
import logging
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from app.deps import require_permission
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/forgejo-error-reporter", tags=["forgejo_error_reporter"])
@router.get("/status")
@router.get("/status", dependencies=[Depends(require_permission("system:read"))])
async def get_status() -> dict:
"""Get the current status of the Forgejo Error Reporter plugin."""
from app.plugins.registry import get_registry
@@ -0,0 +1,33 @@
"""GraphRAG plugin contract — public interface for cross-plugin access."""
from __future__ import annotations
from app.plugins.builtins.contracts import get_contract_registry
from app.plugins.builtins.graph_rag.provider import GraphRAGSearchProvider
class GraphRagContract:
"""Public contract for the graph_rag plugin."""
contract_name = "graph_rag"
GraphRAGSearchProvider = GraphRAGSearchProvider
# ─── self-registration ───
_contract = GraphRagContract()
get_contract_registry().register("graph_rag", _contract)
# Backward-compatible local accessor
_contract_instance: GraphRagContract | None = None
def get_contract() -> GraphRagContract:
global _contract_instance
if _contract_instance is None:
_contract_instance = GraphRagContract()
return _contract_instance
__all__ = ["GraphRagContract", "GraphRAGSearchProvider"]
+2 -2
View File
@@ -37,7 +37,7 @@ class GraphRAGPlugin(BasePlugin):
async def on_activate(self, db, service_container, event_bus) -> None:
"""Activate plugin: register GraphRAG search provider."""
from app.plugins.builtins.graph_rag.provider import GraphRAGSearchProvider
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
from app.plugins.builtins.unified_search.contracts import get_search_registry
registry = get_search_registry()
try:
@@ -50,7 +50,7 @@ class GraphRAGPlugin(BasePlugin):
async def on_deactivate(self, db, service_container, event_bus) -> None:
"""Deactivate plugin: unregister search provider and contract."""
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
from app.plugins.builtins.unified_search.contracts import get_search_registry
get_search_registry().unregister("graph_relationship")
from app.plugins.builtins.contracts import get_contract_registry
+1 -1
View File
@@ -9,7 +9,7 @@ from typing import Any
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider
from app.plugins.builtins.unified_search.contracts import BaseSearchProvider
logger = logging.getLogger(__name__)
@@ -13,7 +13,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.core.storage import get_storage_backend
from app.plugins.builtins.permissions.models import ShareLink
from app.plugins.builtins.dms.models import File as DmsFile
from app.plugins.builtins.dms.contracts import DmsContract
DmsFile = DmsContract.DmsFile
router = APIRouter(prefix="/api/v1/public/share", tags=["public-share"])
@@ -5,6 +5,8 @@ from __future__ import annotations
from app.plugins.builtins.contracts import get_contract_registry
from app.plugins.builtins.unified_search.embedding import generate_embedding
from app.plugins.builtins.unified_search.search_engine import hybrid_search
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider
class UnifiedSearchContract:
@@ -14,6 +16,8 @@ class UnifiedSearchContract:
generate_embedding = staticmethod(generate_embedding)
hybrid_search = staticmethod(hybrid_search)
get_search_registry = staticmethod(get_search_registry)
BaseSearchProvider = BaseSearchProvider
# ─── self-registration ───
@@ -32,4 +36,4 @@ def get_contract() -> UnifiedSearchContract:
return _contract_instance
__all__ = ["UnifiedSearchContract", "generate_embedding", "hybrid_search"]
__all__ = ["UnifiedSearchContract", "generate_embedding", "hybrid_search", "get_search_registry", "BaseSearchProvider"]
@@ -130,9 +130,8 @@ async def auto_register_providers(db: AsyncSession) -> None:
from app.plugins.builtins.unified_search.providers.user_provider import (
UserSearchProvider,
)
from app.plugins.builtins.graph_rag.provider import (
GraphRAGSearchProvider,
)
from app.plugins.builtins.graph_rag.contracts import GraphRagContract
GraphRAGSearchProvider = GraphRagContract.GraphRAGSearchProvider
registry = get_search_registry()
registry.clear()