Delegation¶
Delegation allows a service account to act on behalf of another principal with explicit, database-backed consent. That principal becomes the real_principal (the "who" of the action) while the service account is recorded as the delegate_principal (the "how").
When to Use¶
Use delegation when a service account needs to perform actions on behalf of another principal. Typical use cases:
- AI agents: an AI agent executing back-office operations on behalf of the Alaner who authorized it.
- Backend-to-backend calls initiated by a user: Backend A receives a request from a member and calls Backend B via a service account. On Backend B, the service account acts as a delegate for the originating member so the action is attributed to them.
Delegation vs Impersonation
Impersonation is an Alaner acting as another user (member or Alaner) via the back-office. The actor remains the impersonating Alaner.
Delegation is a service account acting on behalf of another principal. That principal becomes the actor (real_principal), and the service account is tracked as the delegate. This requires explicit consent stored in the database.
How It Works¶
sequenceDiagram
participant SA as Service Account
participant API as Alan Backend
participant Provider as ServiceAccount<br/>AuthContextProvider
participant Policy as DelegationAccessPolicy
SA->>API: Request with Bearer token<br/>+ X-ALAN-DELEGATION-SUBJECT: user@example.com
API->>Provider: set_auth_context_from_request()
Provider->>Provider: Validate Google OAuth token
Provider->>Provider: Look up ServiceAccount principal
Provider->>Provider: Read X-ALAN-DELEGATION-SUBJECT header
Provider->>Provider: Look up delegate target
Provider->>Policy: do_evaluate(service_account, delegate_target)
Policy-->>Provider: ✅ allowed
Provider->>Policy: get_scopes(sa_id, target_id)
Policy-->>Provider: {"scope_a", "scope_b"}
Provider->>API: set_auth_context(<br/> real_principal=target,<br/> delegate_principal=service_account,<br/> session_scopes=scopes)
Request Format¶
The service account sends a standard authenticated request with one additional header:
Source: shared/iam/headers.py — DELEGATION_SUBJECT_HEADER
AuthContext in Delegation¶
When delegation is active, the AuthContext is set up as follows:
| Property | Value | Description |
|---|---|---|
real_principal |
Delegate target | The principal on whose behalf the action is performed |
delegate_principal |
ServiceAccount | The service account performing the action |
is_delegated |
True |
Indicates delegation is active |
session_scopes |
Set of scope strings | Scopes granted by the consent |
from shared.iam.helpers import current_auth_context
# Check if the request is delegated
if current_auth_context.is_delegated:
target = current_auth_context.real_principal # the delegate target
sa = current_auth_context.delegate_principal # the service account
scopes = current_auth_context.session_scopes # granted scopes
Source: shared/iam/auth_context/model.py — AuthContext
DelegationAccessPolicy Protocol¶
The provider delegates authorization to a DelegationAccessPolicy — a protocol with two methods:
class DelegationAccessPolicy(Protocol):
@classmethod
def do_evaluate(
cls, *, service_account: AuthPrincipal, delegate_target: AuthPrincipal
) -> bool:
"""Return True if the service account may delegate to this target."""
...
@classmethod
def get_scopes(
cls, service_account_id: object, delegate_target_id: object
) -> set[str]:
"""Return the set of scopes granted for this delegation pair."""
...
Source: shared/iam/auth_context/providers/service_account.py — DelegationAccessPolicy
Note
Any class that extends AccessPolicy automatically satisfies this protocol — AccessPolicy.do_evaluate() is a @final wrapper that calls the concrete evaluate() method and records metrics.
DelegationConsent Model¶
Consent is a domain concern. The eu-tools app provides a reference implementation with the DelegationConsent model:
| Column | Type | Description |
|---|---|---|
alaner_id |
int (FK) |
The Alaner granting consent |
service_account_id |
UUID (FK) |
The service account receiving consent |
scopes |
DelegationConsentScope[] |
Granted scopes (enum array) |
consented_at |
datetime |
When consent was granted |
revoked_at |
datetime? |
When consent was revoked (NULL = active) |
An ExcludeConstraint ensures no two active consents overlap for the same (alaner, service_account) pair.
Source: apps/eu_tools/alan_home/models/delegation_consent.py — DelegationConsent
Example Access Policy¶
class ServiceAccountDelegationAccessPolicy(AccessPolicy):
"""Validates that a service account is allowed to delegate to a given principal."""
policy_id = "service-account-delegation"
@classmethod
def evaluate(
cls, service_account: AuthPrincipal, delegate_target: AuthPrincipal
) -> bool:
# Both must be active
if not (service_account.is_active and delegate_target.is_active):
return False
# An active DelegationConsent must exist
return get_active_delegation_scopes(service_account.id, delegate_target.id) is not None
@classmethod
def get_scopes(
cls, service_account_id: object, delegate_target_id: object
) -> set[str]:
scopes = get_active_delegation_scopes(service_account_id, delegate_target_id)
return set(scopes) if scopes else set()
Source: apps/eu_tools/access_policies/service_accounts.py — ServiceAccountDelegationAccessPolicy
Configuration¶
Enable delegation on ServiceAccountAuthContextProvider by passing delegate_principal_types and delegation_access_policy:
from shared.iam.auth_context.providers.service_account import ServiceAccountAuthContextProvider
api.register_default_auth_context_providers(
ZeroTrustAuthContextProvider(Alaner, ExternalUser),
ServiceAccountAuthContextProvider(
ServiceAccount,
delegate_principal_types=(Alaner,),
delegation_access_policy=ServiceAccountDelegationAccessPolicy,
),
)
| Parameter | Type | Description |
|---|---|---|
*auth_principal_types |
type[AuthPrincipal] |
Principal types for the service account itself |
delegate_principal_types |
tuple[type[AuthPrincipal], ...] |
Principal types the SA can delegate to (default: empty = delegation disabled) |
delegation_access_policy |
type[DelegationAccessPolicy] \| None |
Policy that authorizes delegation and provides scopes |
Source: shared/iam/auth_context/providers/service_account.py — ServiceAccountAuthContextProvider
Both parameters required
Delegation is only active when both delegate_principal_types and delegation_access_policy are provided. If the X-ALAN-DELEGATION-SUBJECT header is sent to a provider without delegation configured, the request is rejected with a 403.
Error Handling¶
The provider returns 403 Forbidden in the following cases:
| Condition | Message |
|---|---|
Header sent but delegate_principal_types is empty |
Delegation not configured for this provider |
| Delegate target email not found | Invalid delegation subject |
No delegation_access_policy configured |
Delegation access policy not configured |
Policy do_evaluate() returns False |
Delegation denied by policy |