Skip to content

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)
Hold "Alt" / "Option" to enable pan & zoom

Request Format

The service account sends a standard authenticated request with one additional header:

Authorization: Bearer <google_access_token>
X-ALAN-DELEGATION-SUBJECT: user@example.com

Source: shared/iam/headers.pyDELEGATION_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.pyAuthContext

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.pyDelegationAccessPolicy

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.pyDelegationConsent

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.pyServiceAccountDelegationAccessPolicy

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.pyServiceAccountAuthContextProvider

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