Resolve current principals for automation

This commit is contained in:
2026-07-28 15:03:50 +02:00
parent f1d64d247e
commit fdfcfbb440
4 changed files with 338 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from fastapi import Depends, Header, HTTPException, Request, status
from sqlalchemy.orm import Session
@@ -15,6 +15,10 @@ from govoplan_core.core.access import (
PrincipalRef,
PrincipalResolver,
)
from govoplan_core.core.automation import (
AutomationPrincipalRequest,
AutomationPrincipalResolution,
)
from govoplan_core.core.identity import IdentityDirectory
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
from govoplan_core.core.organizations import OrganizationDirectory
@@ -558,6 +562,149 @@ class AccessApiPrincipalProvider:
)
class AccessAutomationPrincipalProvider:
"""Rebuild a trigger owner against current tenant authorization."""
def __init__(
self,
*,
idm_directory: IdmDirectory | None = None,
identity_directory: IdentityDirectory | None = None,
organization_directory: OrganizationDirectory | None = None,
) -> None:
self._idm_directory = idm_directory
self._identity_directory = identity_directory
self._organization_directory = organization_directory
def resolve_automation_principal(
self,
session: object,
*,
request: AutomationPrincipalRequest,
) -> AutomationPrincipalResolution:
if not isinstance(session, Session):
raise TypeError(
"Access automation principal resolution requires a "
"SQLAlchemy session"
)
account = session.get(Account, request.account_id)
user = session.get(User, request.membership_id)
tenant = session.get(Tenant, request.tenant_id)
if (
account is None
or user is None
or tenant is None
or not account.is_active
or not user.is_active
or not tenant.is_active
or user.account_id != account.id
or user.tenant_id != tenant.id
):
return AutomationPrincipalResolution(
allowed=False,
reason=(
"The automation owner is inactive, missing, or no longer "
"belongs to the tenant."
),
missing_scopes=tuple(sorted(set(request.grant_scopes))),
provenance={
"authorization_ref": request.authorization_ref,
"tenant_id": request.tenant_id,
"account_id": request.account_id,
"membership_id": request.membership_id,
"status": "inactive_or_inconsistent",
},
)
idm_assignments, idm_roles = _principal_idm_context(
session,
user=user,
account=account,
tenant_id=request.tenant_id,
idm_directory=self._idm_directory,
organization_directory=self._organization_directory,
)
authorization_context = collect_user_authorization_context(
session,
user,
account=account,
include_system=False,
extra_roles=idm_roles,
)
current_scopes = authorization_context.scopes
granted_scopes = tuple(
sorted(
{
required
for required in request.grant_scopes
if scopes_grant_compatible(current_scopes, required)
}
)
)
missing_scopes = tuple(
sorted(set(request.grant_scopes) - set(granted_scopes))
)
if missing_scopes:
return AutomationPrincipalResolution(
allowed=False,
reason=(
"The automation owner no longer has every scope granted "
"to this trigger."
),
granted_scopes=granted_scopes,
missing_scopes=missing_scopes,
provenance={
"authorization_ref": request.authorization_ref,
"tenant_id": request.tenant_id,
"account_id": request.account_id,
"membership_id": request.membership_id,
"status": "authorization_reduced",
},
)
principal_ref = _build_principal_ref(
session,
account=account,
user=user,
tenant_id=request.tenant_id,
scopes=list(granted_scopes),
auth_method="service_account",
idm_assignments=idm_assignments,
identity_directory=self._identity_directory,
extra_roles=idm_roles,
authorization_context=authorization_context,
include_system_roles=False,
)
principal_ref = replace(
principal_ref,
service_account_id=request.authorization_ref,
acting_for_account_id=account.id,
)
api_principal = _api_principal_from_ref(
session,
principal_ref,
permission_evaluator=LegacyPermissionEvaluator(),
)
return AutomationPrincipalResolution(
allowed=True,
principal=api_principal,
granted_scopes=granted_scopes,
provenance={
"authorization_ref": request.authorization_ref,
"tenant_id": request.tenant_id,
"account_id": request.account_id,
"membership_id": request.membership_id,
"role_ids": sorted(principal_ref.role_ids),
"group_ids": sorted(principal_ref.group_ids),
"function_assignment_ids": sorted(
principal_ref.function_assignment_ids
),
"delegation_ids": sorted(principal_ref.delegation_ids),
"status": "current_authorization_resolved",
},
)
def get_api_principal(
request: Request,
session: Session = Depends(get_session),

View File

@@ -15,6 +15,7 @@ from govoplan_core.core.access import (
CAPABILITY_ACCESS_TENANT_PROVISIONER,
CAPABILITY_ACCESS_SEMANTIC_DIRECTORY,
CAPABILITY_AUTH_API_PRINCIPAL_PROVIDER,
CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_TENANT_CONTEXT_SWITCHER,
@@ -479,6 +480,18 @@ def _api_principal_provider(context: ModuleContext) -> object:
return AccessApiPrincipalProvider()
def _automation_principal_provider(context: ModuleContext) -> object:
from govoplan_access.backend.auth.dependencies import (
AccessAutomationPrincipalProvider,
)
return AccessAutomationPrincipalProvider(
idm_directory=_optional_idm_directory(context),
identity_directory=_optional_identity_directory(context),
organization_directory=_optional_organization_directory(context),
)
def _tenant_context_switcher(context: ModuleContext) -> object:
del context
from govoplan_access.backend.auth.tenant_context import AccessTenantContextSwitcher
@@ -612,6 +625,10 @@ manifest = ModuleManifest(
optional_dependencies=("identity", "organizations", "tenancy", "idm"),
provides_interfaces=(
ModuleInterfaceProvider(name=CAPABILITY_ACCESS_PEOPLE_SEARCH, version="0.1.0"),
ModuleInterfaceProvider(
name="auth.automation_principal",
version="0.1.0",
),
),
permissions=ACCESS_PERMISSIONS,
role_templates=ACCESS_ROLE_TEMPLATES,
@@ -653,6 +670,9 @@ manifest = ModuleManifest(
),
capability_factories={
CAPABILITY_AUTH_API_PRINCIPAL_PROVIDER: _api_principal_provider,
CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER: (
_automation_principal_provider
),
CAPABILITY_AUTH_PRINCIPAL_RESOLVER: _legacy_principal_resolver,
CAPABILITY_AUTH_PERMISSION_EVALUATOR: _legacy_permission_evaluator,
CAPABILITY_AUTH_TENANT_CONTEXT_SWITCHER: _tenant_context_switcher,