from __future__ import annotations from collections.abc import Iterable from functools import wraps from flask import current_app, g, jsonify, request from app.models.result import failed READ_GOVERNANCE = "governance:read" EDIT_GOVERNANCE = "governance:edit" APPROVE_REVIEW = "review:approve" MANAGE_USERS = "users:manage" ACTIVATE_WORKFLOW = "workflow:activate" OPERATE_ORDERS = "orders:operate" DATASOURCE_POOL_MANAGE = "datasources:pools:manage" RESPONSIBILITIES_READ = "governance:responsibilities:read" RESPONSIBILITIES_MANAGE = "governance:responsibilities:manage" KNOWLEDGE_MANAGE = "knowledge:manage" RULES_READ = "rules:read" RULES_EDIT = "rules:edit" RULES_PUBLISH = "rules:publish" DATAFLOW_RELEASE = "dataflows:release" DATAFLOW_DEPLOY = "dataflows:deploy" DATAFLOW_CANARY = "dataflows:canary" DATAFLOW_ACTIVATE = "dataflows:activate" DATAFLOW_ROLLBACK = "dataflows:rollback" RULES_EXECUTE = "rules:execute" INGESTION_RUN = "ingestion:run" INGESTION_ADMIN = "ingestion:admin" EVIDENCE_DOWNLOAD = "evidence:download" DATA_ELEMENTS_EDIT = "data-elements:edit" DATA_ELEMENTS_PUBLISH = "data-elements:publish" ONTOLOGIES_EDIT = "ontologies:edit" ONTOLOGIES_PUBLISH = "ontologies:publish" DEVICE_ASSETS_EDIT = "device-assets:edit" DEVICE_SEMANTICS_EDIT = "device-semantics:edit" DEVICE_SEMANTICS_REVIEW = "device-semantics:review" DEVICE_ENTITIES_EDIT = "device-entities:edit" DEVICE_ENTITIES_REVIEW = "device-entities:review" DEVICE_QUALITY_EDIT = "device-quality:edit" DEVICE_QUALITY_EXECUTE = "device-quality:execute" DEVICE_QUALITY_PUBLISH = "device-quality:publish" QUALITY_ISSUES_EDIT = "quality-issues:edit" QUALITY_ISSUES_REVIEW = "quality-issues:review" DEVICE_OBSERVABILITY_EDIT = "device-observability:edit" GOVERNANCE_AUDIT_READ = "governance-audit:read" GOVERNANCE_AUDIT_SEAL = "governance-audit:seal" DOMAIN_TEMPLATES_READ = "domain-templates:read" DOMAIN_TEMPLATES_PREVIEW = "domain-templates:preview" DOMAIN_TEMPLATES_MANAGE = "domain-templates:manage" ACTIVE_METADATA_READ = "active-metadata:read" ACTIVE_METADATA_OPERATE = "active-metadata:operate" ACTIVE_METADATA_MANAGE = "active-metadata:manage" SEMANTIC_GOVERNANCE_EDIT = "semantic-governance:edit" SEMANTIC_GOVERNANCE_REVIEW = "semantic-governance:review" SEMANTIC_GOVERNANCE_PUBLISH = "semantic-governance:publish" DATA_OBSERVABILITY_READ = "data-observability:read" DATA_OBSERVABILITY_OPERATE = "data-observability:operate" DATA_OBSERVABILITY_MANAGE = "data-observability:manage" WORK_CENTER_READ = "governance:work-center:read" WORK_CENTER_OPERATE = "governance:work-center:operate" WORK_CENTER_MANAGE = "governance:work-center:manage" DATA_PRODUCTS_READ = "data-products:read" DATA_PRODUCTS_OPERATE = "data-products:operate" DATA_PRODUCTS_MANAGE = "data-products:manage" AGENTS_READ = "agents:read" AGENTS_OPERATE = "agents:operate" AGENTS_MANAGE = "agents:manage" SECURITY_GOVERNANCE_READ = "security-governance:read" SECURITY_GOVERNANCE_OPERATE = "security-governance:operate" SECURITY_GOVERNANCE_MANAGE = "security-governance:manage" IDENTITY_READ = "identity:read" IDENTITY_MANAGE = "identity:manage" IDENTITY_OPERATE = "identity:operate" CONNECTORS_READ = "connectors:read" CONNECTORS_OPERATE = "connectors:operate" CONNECTORS_MANAGE = "connectors:manage" EDGE_GATEWAYS_READ = "edge-gateways:read" EDGE_GATEWAYS_OPERATE = "edge-gateways:operate" EDGE_GATEWAYS_MANAGE = "edge-gateways:manage" BI_AI_CATALOG_READ = "bi-ai-catalog:read" BI_AI_CATALOG_MANAGE = "bi-ai-catalog:manage" METERING_READ = "metering:read" METERING_MANAGE = "metering:manage" ROLE_PERMISSIONS = { "viewer": frozenset( { READ_GOVERNANCE, RULES_READ, RESPONSIBILITIES_READ, DOMAIN_TEMPLATES_READ, ACTIVE_METADATA_READ, DATA_OBSERVABILITY_READ, WORK_CENTER_READ, DATA_PRODUCTS_READ, AGENTS_READ, SECURITY_GOVERNANCE_READ, IDENTITY_READ, CONNECTORS_READ, EDGE_GATEWAYS_READ, BI_AI_CATALOG_READ, METERING_READ, } ), "editor": frozenset( { READ_GOVERNANCE, EDIT_GOVERNANCE, APPROVE_REVIEW, OPERATE_ORDERS, RULES_READ, RULES_EDIT, RULES_EXECUTE, RESPONSIBILITIES_READ, INGESTION_RUN, DATA_ELEMENTS_EDIT, ONTOLOGIES_EDIT, DEVICE_ASSETS_EDIT, DEVICE_SEMANTICS_EDIT, DEVICE_ENTITIES_EDIT, DEVICE_QUALITY_EDIT, DEVICE_QUALITY_EXECUTE, QUALITY_ISSUES_EDIT, DEVICE_OBSERVABILITY_EDIT, DOMAIN_TEMPLATES_READ, DOMAIN_TEMPLATES_PREVIEW, ACTIVE_METADATA_READ, ACTIVE_METADATA_OPERATE, SEMANTIC_GOVERNANCE_EDIT, DATA_OBSERVABILITY_READ, DATA_OBSERVABILITY_OPERATE, WORK_CENTER_READ, WORK_CENTER_OPERATE, DATA_PRODUCTS_READ, DATA_PRODUCTS_OPERATE, AGENTS_READ, AGENTS_OPERATE, SECURITY_GOVERNANCE_READ, SECURITY_GOVERNANCE_OPERATE, IDENTITY_READ, IDENTITY_OPERATE, CONNECTORS_READ, CONNECTORS_OPERATE, EDGE_GATEWAYS_READ, EDGE_GATEWAYS_OPERATE, BI_AI_CATALOG_READ, METERING_READ, } ), "admin": frozenset( { READ_GOVERNANCE, EDIT_GOVERNANCE, APPROVE_REVIEW, MANAGE_USERS, ACTIVATE_WORKFLOW, OPERATE_ORDERS, DATASOURCE_POOL_MANAGE, RESPONSIBILITIES_READ, RESPONSIBILITIES_MANAGE, KNOWLEDGE_MANAGE, RULES_READ, RULES_EDIT, RULES_PUBLISH, DATAFLOW_RELEASE, DATAFLOW_DEPLOY, DATAFLOW_CANARY, DATAFLOW_ACTIVATE, DATAFLOW_ROLLBACK, RULES_EXECUTE, INGESTION_RUN, INGESTION_ADMIN, EVIDENCE_DOWNLOAD, DATA_ELEMENTS_EDIT, DATA_ELEMENTS_PUBLISH, ONTOLOGIES_EDIT, ONTOLOGIES_PUBLISH, DEVICE_ASSETS_EDIT, DEVICE_SEMANTICS_EDIT, DEVICE_SEMANTICS_REVIEW, DEVICE_ENTITIES_EDIT, DEVICE_ENTITIES_REVIEW, DEVICE_QUALITY_EDIT, DEVICE_QUALITY_EXECUTE, DEVICE_QUALITY_PUBLISH, QUALITY_ISSUES_EDIT, QUALITY_ISSUES_REVIEW, DEVICE_OBSERVABILITY_EDIT, GOVERNANCE_AUDIT_READ, GOVERNANCE_AUDIT_SEAL, DOMAIN_TEMPLATES_READ, DOMAIN_TEMPLATES_PREVIEW, DOMAIN_TEMPLATES_MANAGE, ACTIVE_METADATA_READ, ACTIVE_METADATA_OPERATE, ACTIVE_METADATA_MANAGE, SEMANTIC_GOVERNANCE_EDIT, SEMANTIC_GOVERNANCE_REVIEW, SEMANTIC_GOVERNANCE_PUBLISH, DATA_OBSERVABILITY_READ, DATA_OBSERVABILITY_OPERATE, DATA_OBSERVABILITY_MANAGE, WORK_CENTER_READ, WORK_CENTER_OPERATE, WORK_CENTER_MANAGE, DATA_PRODUCTS_READ, DATA_PRODUCTS_OPERATE, DATA_PRODUCTS_MANAGE, AGENTS_READ, AGENTS_OPERATE, AGENTS_MANAGE, SECURITY_GOVERNANCE_READ, SECURITY_GOVERNANCE_OPERATE, SECURITY_GOVERNANCE_MANAGE, IDENTITY_READ, IDENTITY_OPERATE, IDENTITY_MANAGE, CONNECTORS_READ, CONNECTORS_OPERATE, CONNECTORS_MANAGE, EDGE_GATEWAYS_READ, EDGE_GATEWAYS_OPERATE, EDGE_GATEWAYS_MANAGE, BI_AI_CATALOG_READ, BI_AI_CATALOG_MANAGE, METERING_READ, METERING_MANAGE, } ), } PUBLIC = "public" def permission_for_request(path: str, method: str) -> tuple[str, ...]: """Classify every API request in one auditable, deny-by-default policy.""" method = method.upper() public_methods = { ("/api/system/health", "GET"), ("/api/system/auth/login", "POST"), ("/api/system/identity/providers", "GET"), ("/api/system/identity/authorize", "POST"), ("/api/system/identity/callback", "GET"), ("/api/system/identity/exchange", "POST"), ("/api/system/identity/refresh", "POST"), ("/api/datasource/connectors/machine/runs", "POST"), ("/api/datasource/edge/register", "POST"), } if (path, method) in public_methods: return (PUBLIC,) if ( path.startswith("/api/datasource/connectors/machine/runs/") and path.rsplit("/", 1)[-1] in {"cancel", "resume"} and method == "POST" ): return (PUBLIC,) if path.startswith("/api/datasource/edge/gateways/") and method == "POST": suffix = path.rsplit("/", 1)[-1] if suffix in {"heartbeat", "pull", "events", "ack", "reconcile", "outcome"}: return (PUBLIC,) if path.startswith("/api/datasource/edge"): if path == "/api/datasource/edge/gateways" and method == "GET": return (EDGE_GATEWAYS_READ,) if path == "/api/datasource/edge/enrollments" and method == "POST": return (EDGE_GATEWAYS_MANAGE,) if path == "/api/datasource/edge/tasks" and method == "POST": return (EDGE_GATEWAYS_OPERATE,) if ( path.startswith("/api/datasource/edge/tasks/") and path.endswith("/cancel") and method == "POST" ): return (EDGE_GATEWAYS_OPERATE,) if method == "GET": return (EDGE_GATEWAYS_READ,) return (EDGE_GATEWAYS_MANAGE,) if path.startswith("/api/system/identity"): if path == "/api/system/identity/logout" and method == "POST": return (IDENTITY_READ,) if ( path in {"/api/system/identity/idp-versions", "/api/system/identity/audit"} and method == "GET" ): return (IDENTITY_MANAGE,) if path == "/api/system/identity/sessions" and method == "GET": return (IDENTITY_OPERATE,) if method == "GET": return (IDENTITY_READ,) if any(marker in path for marker in ("/idp-versions", "/emergency/")): return (IDENTITY_MANAGE,) return (IDENTITY_OPERATE,) if path.startswith("/api/system/tenant/"): return (IDENTITY_OPERATE,) if path.startswith("/api/system/bi-ai-catalog/"): return ( (BI_AI_CATALOG_READ,) if method == "GET" or path.endswith("/search") else (BI_AI_CATALOG_MANAGE,) ) if path.startswith("/api/system/metering/"): return (METERING_READ,) if method == "GET" else (METERING_MANAGE,) if path.startswith("/api/datasource/connectors"): if "/source-bindings" in path: return (CONNECTORS_MANAGE,) if method == "GET": return (CONNECTORS_READ,) if any(marker in path for marker in ("/principals", "/credentials/")): return (CONNECTORS_MANAGE,) return (CONNECTORS_OPERATE,) if path == "/api/datasource/graph": return (CONNECTORS_READ,) if path.startswith("/api/system/responsibilities/"): if method == "GET": return (RESPONSIBILITIES_READ,) return (RESPONSIBILITIES_MANAGE,) if path.startswith("/api/system/work-center"): if method == "GET": return (WORK_CENTER_READ,) if any( segment in path for segment in ("/workflows", "/templates", "/timeouts", "/deliver") ): return (WORK_CENTER_MANAGE,) return (WORK_CENTER_OPERATE,) if path.startswith("/api/system/governance-audit"): if method == "GET": return (GOVERNANCE_AUDIT_READ,) return (GOVERNANCE_AUDIT_SEAL,) if path.startswith("/api/system/security-governance"): if method == "GET": return (SECURITY_GOVERNANCE_READ,) if any( marker in path for marker in ( "/findings/", "/reconcile", "/vulnerabilities/", "/dispatch", ) ): return (SECURITY_GOVERNANCE_MANAGE,) if any( path.startswith(prefix) for prefix in ( "/api/system/security-governance/retention", "/api/system/security-governance/siem", "/api/system/security-governance/sboms", ) ): return (SECURITY_GOVERNANCE_MANAGE,) return (SECURITY_GOVERNANCE_OPERATE,) if path.startswith("/api/system/trusted-delivery"): if path.startswith("/api/system/trusted-delivery/controls/"): if method == "GET": return (SECURITY_GOVERNANCE_READ,) if any( marker in path for marker in ( "/profiles/activate", "/profiles/rollback", "/capability-approvals", "/destruction-approvals", ) ): return (SECURITY_GOVERNANCE_MANAGE,) return (SECURITY_GOVERNANCE_OPERATE,) if "/legal-holds" in path or "/reclaim/" in path: return (SECURITY_GOVERNANCE_MANAGE,) return (SECURITY_GOVERNANCE_OPERATE,) if path.startswith("/api/dataservice/governance"): if method == "GET": return (DATA_PRODUCTS_READ,) if ( any( marker in path for marker in ( "/reconcile", "/fulfill", ) ) or path == "/api/dataservice/governance/products" ): return (DATA_PRODUCTS_MANAGE,) return (DATA_PRODUCTS_OPERATE,) if path.startswith("/api/knowledge/agents"): if method == "GET": return (AGENTS_READ,) if path == "/api/knowledge/agents" or path.endswith("/reconcile"): return (AGENTS_MANAGE,) return (AGENTS_OPERATE,) if path.startswith("/api/meta/domain-templates"): if method == "GET": return (DOMAIN_TEMPLATES_READ,) if path == "/api/meta/domain-templates/dry-run": return (DOMAIN_TEMPLATES_PREVIEW,) return (DOMAIN_TEMPLATES_MANAGE,) if path.startswith("/api/meta/active-metadata"): if method == "GET": return (ACTIVE_METADATA_READ,) if path == "/api/meta/active-metadata/plans": return (ACTIVE_METADATA_MANAGE,) return (ACTIVE_METADATA_OPERATE,) if path.startswith("/api/datafactory/observability"): if method == "GET": return (DATA_OBSERVABILITY_READ,) if path == "/api/datafactory/observability/slos": return (DATA_OBSERVABILITY_MANAGE,) return (DATA_OBSERVABILITY_OPERATE,) if path in {"/api/knowledge/search", "/api/knowledge/ask"}: return (READ_GOVERNANCE,) if path.startswith("/api/rules"): if path.startswith("/api/rules/deployments"): if method == "GET": return (RULES_READ,) if path.endswith("/deploy-disabled"): return (DATAFLOW_DEPLOY,) if path.endswith("/canary"): return (DATAFLOW_CANARY, RULES_EXECUTE) if path.endswith("/execute"): return (RULES_EXECUTE,) if path.endswith("/activate"): return (DATAFLOW_ACTIVATE,) if path.endswith("/rollback"): return (DATAFLOW_ROLLBACK,) if path.endswith("/reconcile-deploy"): return (DATAFLOW_DEPLOY,) if path.endswith("/reconcile-activate"): return (DATAFLOW_ACTIVATE,) if path.endswith("/reconcile-rollback"): return (DATAFLOW_ROLLBACK,) if path.endswith("/reconcile-execute"): return (RULES_EXECUTE,) if method == "POST": return (DATAFLOW_DEPLOY,) return (DATAFLOW_DEPLOY,) if method == "GET": return (RULES_READ,) if path.endswith("/publish"): return (RULES_PUBLISH,) if path.startswith("/api/rules/production-lines/") and path.endswith( "/release" ): return (DATAFLOW_RELEASE,) if path.endswith("/test") or path.endswith("/execute"): return (RULES_EXECUTE,) if method in {"POST", "PUT", "PATCH", "DELETE"}: return (RULES_EDIT,) return (RULES_PUBLISH,) if path.startswith("/api/knowledge/admin"): return (KNOWLEDGE_MANAGE,) if path.startswith("/api/system/users"): return (MANAGE_USERS,) if path.startswith("/api/development/v1/governance-metrics"): return (READ_GOVERNANCE,) if path.startswith("/api/development/v1/semantic"): if method == "GET": return (READ_GOVERNANCE,) if path.endswith("/review"): return (SEMANTIC_GOVERNANCE_REVIEW,) if path.endswith("/publish") or path.endswith("/rollback"): return (SEMANTIC_GOVERNANCE_PUBLISH,) return (SEMANTIC_GOVERNANCE_EDIT,) if path.startswith("/api/development/v1/device-assets"): if method == "GET": return (READ_GOVERNANCE,) return (DEVICE_ASSETS_EDIT,) if path.startswith("/api/development/v1/device-semantics"): if method == "GET": return (READ_GOVERNANCE,) if path.endswith("/review"): return (DEVICE_SEMANTICS_REVIEW,) return (DEVICE_SEMANTICS_EDIT,) if path.startswith("/api/development/v1/device-entities"): if method == "GET": return (READ_GOVERNANCE,) if path.endswith("/review") or path.endswith("/rollback"): return (DEVICE_ENTITIES_REVIEW,) return (DEVICE_ENTITIES_EDIT,) if path.startswith("/api/development/v1/device-observability"): if method == "GET": return (READ_GOVERNANCE,) return (DEVICE_OBSERVABILITY_EDIT,) if path.startswith("/api/development/v1/device-quality"): if method == "GET": return (READ_GOVERNANCE,) if path.startswith("/api/development/v1/device-quality/issues"): if path.endswith("/review"): return (QUALITY_ISSUES_REVIEW,) return (QUALITY_ISSUES_EDIT,) if path.endswith("/publish"): return (DEVICE_QUALITY_PUBLISH,) if path == "/api/development/v1/device-quality/runs": return (DEVICE_QUALITY_EXECUTE,) return (DEVICE_QUALITY_EDIT,) if path.startswith("/api/development/v1/ingestion-jobs"): if method == "GET": return (READ_GOVERNANCE,) if path.endswith("/retry"): return (INGESTION_ADMIN,) if path.endswith("/cancel"): return (INGESTION_RUN,) if method == "POST": return (INGESTION_RUN,) if path.startswith("/api/development/v1/data-elements"): if method == "GET": return (READ_GOVERNANCE,) return (DATA_ELEMENTS_EDIT,) if path.startswith("/api/development/v1/candidate-decisions"): return (DATA_ELEMENTS_EDIT,) if path.startswith("/api/development/v1/sources/files"): return (INGESTION_RUN,) if path.startswith("/api/development/v1/evidence"): return (READ_GOVERNANCE,) if path.startswith("/api/development/v1/ontologies"): if method == "GET": return (READ_GOVERNANCE,) if path.endswith("/publish") or path.endswith("/rollback"): return (ONTOLOGIES_PUBLISH,) return (ONTOLOGIES_EDIT,) if path.startswith("/api/bd/"): if path in { "/api/bd/list", "/api/bd/detail", "/api/bd/graphall", "/api/bd/search", }: return (READ_GOVERNANCE,) return (EDIT_GOVERNANCE,) if path.startswith("/api/system/workbench"): return (READ_GOVERNANCE,) if path == "/api/datasource/pools" or ( path.startswith("/api/datasource/") and (path.endswith("/pool") or path.endswith("/pool/invalidate")) ): return (DATASOURCE_POOL_MANAGE,) if path == "/api/datasource/list": return (READ_GOVERNANCE,) if path.startswith("/api/system/") and not path.startswith("/api/system/auth/me"): return (MANAGE_USERS,) if path.startswith("/api/dataflow/") and any( marker in path.lower() for marker in ("activate", "publish", "execute") ): return (ACTIVATE_WORKFLOW,) if "order" in path.lower() and method != "GET": return (OPERATE_ORDERS,) if method == "GET": return (READ_GOVERNANCE,) if method in {"POST", "PUT", "PATCH", "DELETE"}: return (EDIT_GOVERNANCE,) return (MANAGE_USERS,) def permissions_for_roles(roles: Iterable[str]) -> frozenset[str]: result: set[str] = set() for role in roles: result.update(ROLE_PERMISSIONS.get(role, ())) return frozenset(result) def authenticate_request() -> dict | None: from app.core.system.auth import load_identity_from_token existing = getattr(g, "current_user", None) if existing: return existing header = request.headers.get("Authorization", "") if not header.startswith("Bearer "): return None token = header[7:].strip() if not token: return None return load_identity_from_token(token, secret=current_app.config["SECRET_KEY"]) def configure_api_authorization(app) -> None: @app.before_request def enforce_api_policy(): if not request.path.startswith("/api/") or request.method == "OPTIONS": return None required = permission_for_request(request.path, request.method) if required == (PUBLIC,): return None identity = authenticate_request() if identity is None: return jsonify(failed("未登录或登录已过期", code=401)), 401 permissions = permissions_for_roles(identity["roles"]) if not set(required).issubset(permissions): return jsonify(failed("权限不足", code=403)), 403 identity["permissions"] = sorted(permissions) g.current_user = identity return None def require_permissions(*required: str): def decorator(view): @wraps(view) def wrapped(*args, **kwargs): identity = authenticate_request() if identity is None: return jsonify(failed("未登录或登录已过期", code=401)), 401 permissions = permissions_for_roles(identity["roles"]) if not set(required).issubset(permissions): return jsonify(failed("权限不足", code=403)), 403 identity["permissions"] = sorted(permissions) g.current_user = identity return view(*args, **kwargs) wrapped.required_permissions = tuple(required) return wrapped return decorator