Resolve current principals for automation
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user