Contrôle d’accès

Contrôle d’accès de niveau entreprise pour les agents IA utilisant adk-auth.

Vue d’ensemble

adk-auth fournit le contrôle d’accès basé sur les rôles (RBAC), l’autorisation basée sur les portées, la journalisation d’audit et la prise en charge de SSO pour les agents ADK. Cela permet un contrôle sécurisé et granulaire des outils auxquels chaque utilisateur peut accéder.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Agent Request                             │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     SSO Token Validation                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │
│  │ Google      │  │ Azure AD    │  │ OIDC Discovery          │  │
│  │ Provider    │  │ Provider    │  │ (Okta, Auth0, etc)     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘  │
│                          │                                       │
│                   ┌──────┴──────┐                                │
│                   │ JWKS Cache  │  ← Auto-refresh keys          │
│                   └─────────────┘                                │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼ TokenClaims
┌─────────────────────────────────────────────────────────────────┐
│                       Claims Mapper                              │
│                                                                  │
│    IdP Groups          →        adk-auth Roles                  │
│    ─────────────────────────────────────────                    │
│    "AdminGroup"        →        "admin"                         │
│    "DataAnalysts"      →        "analyst"                       │
│    (default)           →        "viewer"                        │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼ Roles
┌─────────────────────────────────────────────────────────────────┐
│                      Access Control                              │
│                                                                  │
│    Role: admin                                                   │
│    ├── allow: AllTools                                          │
│    └── allow: AllAgents                                         │
│                                                                  │
│    Role: analyst                                                 │
│    ├── allow: Tool("search")                                    │
│    ├── allow: Tool("summarize")                                 │
│    └── deny:  Tool("code_exec")  ← Deny takes precedence        │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼ Check Result
┌─────────────────────────────────────────────────────────────────┐
│                      Audit Logging                               │
│                                                                  │
│    {"user":"alice","resource":"search","outcome":"allowed"}     │
│    {"user":"bob","resource":"exec","outcome":"denied"}          │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Tool Execution                               │
│               (only if access granted)                          │
└─────────────────────────────────────────────────────────────────┘

Principes de conception

1. La priorité au refus

Lorsqu’un rôle contient à la fois des règles d’autorisation et de refus, le refus l’emporte toujours :

let role = Role::new("limited")
    .allow(Permission::AllTools)      // Allow everything...
    .deny(Permission::Tool("admin")); // ...except admin

// Result: Can access any tool EXCEPT "admin"

2. Union de plusieurs rôles

Les utilisateurs ayant plusieurs rôles obtiennent l’union des permissions, mais les règles de refus de n’importe quel rôle s’appliquent toujours :

let ac = AccessControl::builder()
    .role(reader)    // allow: search
    .role(writer)    // allow: write
    .assign("alice", "reader")
    .assign("alice", "writer")
    .build()?;

// Alice can access both "search" AND "write"

3. Explicite plutôt qu’implicite

Les permissions sont explicites - aucun accès n’est accordé par défaut :

let role = Role::new("empty");
// This role grants NO permissions

ac.check("user", &Permission::Tool("anything")); // → Denied

4. Séparation de l’authentification et de l’autorisation

  • Authentification (SSO) : Vérifie QUI est l’utilisateur (via JWT)
  • Autorisation (RBAC) : Détermine À QUOI il peut accéder
// Authentication: validate JWT, extract claims
let claims = provider.validate(token).await?;

// Authorization: check specific permission
ac.check(&claims.sub, &Permission::Tool("search"))?;

// Combined: SsoAccessControl does both
sso.check_token(token, &permission).await?;

Installation

[dependencies]
adk-auth = "2.0.0"

# For SSO/OAuth support
adk-auth = { version = "2.0.0", features = ["sso"] }

Composants principaux

Permission

pub enum Permission {
    Tool(String),     // Specific tool by name
    AllTools,         // Wildcard: all tools
    Agent(String),    // Specific agent by name  
    AllAgents,        // Wildcard: all agents
}

Rôle

let analyst = Role::new("analyst")
    .allow(Permission::Tool("search".into()))
    .allow(Permission::Tool("summarize".into()))
    .deny(Permission::Tool("code_exec".into()));

AccessControl

let ac = AccessControl::builder()
    .role(admin)
    .role(analyst)
    .assign("alice@company.com", "admin")
    .assign("bob@company.com", "analyst")
    .build()?;

// Check permission
ac.check("bob@company.com", &Permission::Tool("search".into()))?;

ProtectedTool

Enveloppe un outil avec une vérification automatique des permissions :

use adk_auth::ToolExt;

let protected = my_tool.with_access_control(Arc::new(ac));

// When executed, checks permission before running
protected.execute(ctx, args).await?;

AuthMiddleware

Protège en lot plusieurs outils :

let middleware = AuthMiddleware::new(ac);
let protected_tools = middleware.protect_all(tools);

ScopeGuard

Utilisez des portées pour l’autorisation au niveau de la requête provenant des revendications JWT ou de l’état de session :

use adk_auth::{ContextScopeResolver, ScopeGuard};

let guard = ScopeGuard::new(ContextScopeResolver);
let protected = guard.protect(my_tool);

Combinaison de RBAC + Portées

RBAC répond à « cet utilisateur peut-il accéder à l’outil, oui ou non ? » Les portées répondent à « cette requête spécifique est-elle autorisée maintenant ? »

use std::sync::Arc;
use adk_auth::{AuthMiddleware, ContextScopeResolver, ScopeGuard};

let rbac = AuthMiddleware::new(ac);
let scoped = ScopeGuard::new(ContextScopeResolver);

let protected = scoped.protect(rbac.protect(transfer_tool));

Intégration SSO

Fournisseurs pris en charge

FournisseurConstructeurÉmetteur
GoogleGoogleProvider::new(client_id)accounts.google.com
Azure ADAzureADProvider::new(tenant, client) or AzureADProvider::multi_tenant(client).with_allowed_tenants(["tenant-id"])login.microsoftonline.com
OktaOktaProvider::new(domain, client){domain}/oauth2/default
Auth0Auth0Provider::new(domain, audience){domain}/
GénériqueOidcProvider::from_discovery(issuer, client)Tout fournisseur OIDC

AzureADProvider::multi_tenant() accepte n’importe quel tenant pour l’audience configurée, sauf si vous le restreignez explicitement avec with_allowed_tenants(...).

TokenClaims

Revendications extraites de JWTs validé :

pub struct TokenClaims {
    pub sub: String,              // Subject (user ID)
    pub email: Option<String>,    // Email
    pub name: Option<String>,     // Display name
    pub groups: Vec<String>,      // IdP groups
    pub roles: Vec<String>,       // IdP roles
    pub hd: Option<String>,       // Google hosted domain
    pub tid: Option<String>,      // Azure tenant ID
    // ... more standard OIDC claims
}

ClaimsMapper

Mappe les groupes IdP aux rôles adk-auth :

let mapper = ClaimsMapper::builder()
    .map_group("AdminGroup", "admin")
    .map_group("Users", "viewer")
    .default_role("guest")
    .user_id_from_email()
    .build();

user_id_from_email() n’utilise le champ de revendication email que lorsque email_verified == true ; sinon, il se rabat sur sub.

SsoAccessControl

Combine la validation de SSO avec RBAC en un seul appel :

let sso = SsoAccessControl::builder()
    .validator(GoogleProvider::new("client-id"))
    .mapper(mapper)
    .access_control(ac)
    .audit_sink(audit)
    .build()?;

// Validate token + check permission + audit log
let claims = sso.check_token(token, &Permission::Tool("search".into())).await?;

Journalisation d’audit

FileAuditSink

let audit = FileAuditSink::new("/var/log/adk/audit.jsonl")?;
let middleware = AuthMiddleware::with_audit(ac, audit);

Format de sortie (JSONL)

{"timestamp":"2025-01-01T10:30:00Z","user":"bob","session_id":"sess-123","event_type":"tool_access","resource":"search","outcome":"allowed"}
{"timestamp":"2025-01-01T10:30:01Z","user":"bob","session_id":"sess-123","event_type":"tool_access","resource":"code_exec","outcome":"denied"}

Récepteur d’audit personnalisé

use adk_auth::{AuditSink, AuditEvent, AuthError};
use async_trait::async_trait;

pub struct DatabaseAuditSink { /* ... */ }

#[async_trait]
impl AuditSink for DatabaseAuditSink {
    async fn log(&self, event: AuditEvent) -> Result<(), AuthError> {
        // Insert into database
        sqlx::query("INSERT INTO audit_log ...")
            .bind(event.user)
            .bind(event.resource)
            .execute(&self.pool)
            .await?;
        Ok(())
    }
}

Exemples

cargo check -p adk-auth
cargo check -p adk-auth --features sso

Bonnes pratiques de sécurité

PratiqueDescription
Refuser par défautN’accorder que les autorisations explicitement nécessaires
Refus explicitesAjouter des règles de refus pour les opérations dangereuses
Auditer toutActiver la journalisation pour la conformité
Valider côté serveurToujours valider JWTs sur le serveur
Utiliser HTTPSLes points de terminaison JWKS nécessitent des connexions sécurisées
Faire tourner les clésLe cache JWKS se rafraîchit automatiquement toutes les heures
Limiter la durée de vie des jetonsUtiliser des jetons d’accès à courte durée de vie
Restreindre les locataires AzurePour les applications Azure multi-locataires, configurez with_allowed_tenants(...)
Vérifier l’e-mail avant le mappage d’identitéuser_id_from_email() revient désormais à sub lorsque l’e-mail n’est pas vérifié
Prévoir la révocationLa révocation des jetons n’est pas intégrée ; appliquez-la dans un validateur personnalisé si vous avez besoin d’une coupure immédiate
Mettre en cache les recherches de portée coûteusesSi votre ScopeResolver appelle des systèmes externes, mettez le résultat en cache par requête/session

Pont d’authentification

Activez auth-bridge lorsque vous voulez que adk-auth fournisse un extracteur de requêtes réutilisable basé sur JWT pour adk-server :

use adk_auth::auth_bridge::JwtRequestContextExtractor;
use adk_auth::sso::{ClaimsMapper, GoogleProvider};

let extractor = JwtRequestContextExtractor::builder()
    .validator(GoogleProvider::new("client-id"))
    .mapper(ClaimsMapper::builder().user_id_from_email().build())
    .build()?;

L’extracteur valide le jeton Bearer, associe user_id à ClaimsMapper, et transmet les revendications JWT scope / scp vers RequestContext.scopes.

Fournisseurs de secrets

Les outils accèdent aux secrets d’exécution via ToolContext::get_secret et InvocationContext::get_secret. Derrière ceux-ci se trouve adk_auth::secrets::SecretProvider, avec des implémentations cloud derrière des indicateurs de fonctionnalité :

FournisseurFonctionnalité
AWS Secrets Manageraws-secrets
Azure Key Vault (coffre de clés)azure-keyvault
Gestionnaire de secrets GCPgcp-secrets

Attachez-en un à une exécution en l’enveloppant comme un SecretService :

use adk_auth::secrets::{CachedSecretProvider, SecretProvider, SecretServiceAdapter};
use std::sync::Arc;
use std::time::Duration;

// Any SecretProvider — here wrapped in the cache
let cached = Arc::new(CachedSecretProvider::new(provider, Duration::from_secs(300)));
let service = Arc::new(SecretServiceAdapter::new(cached));

Autorisation par outil

Par défaut, un outil qui détient un contexte peut nommer n’importe quel secret, et le fournisseur ne voit que ce nom — rien ne distingue un outil météo demandant sa propre clé API d’une demande du même outil pour un identifiant de paiement. AuthorizingSecretService décide par outil avant que le fournisseur ne soit consulté :

use adk_auth::secrets::authorizing::{AuthorizingSecretService, SecretGrant};
use std::sync::Arc;

let service = Arc::new(
    AuthorizingSecretService::new(inner)
        .grant("weather_lookup", SecretGrant::none().name("weather-api-key"))
        .grant("charge_card", SecretGrant::none().prefix("billing/"))
        .with_audit_sink(audit_sink),
);
RègleComportement
L’outil dispose d’une autorisation couvrant le nomAutorisé
L’outil dispose d’une autorisation qui ne couvre pas le nomRefusé ; le fournisseur n’est jamais appelé
L’outil n’a aucun droitRefusé
La requête ne porte aucune identité d’outilRefusé sauf si grant_untooled l’ouvre

Tout est refusé jusqu’à autorisation, et un refus renvoie une erreur Unauthorized. Un nom refusé n’est jamais recherché, il n’apparaît donc pas dans les journaux d’accès côté fournisseur comme une lecture tentée.

L’identité n’est pas quelque chose qu’un outil affirme. LlmAgent appose le nom de l’outil envoyé sur la requête, avec l’application, l’utilisateur, la session et l’invocation, de sorte qu’un outil ne peut pas présenter l’identité d’un autre outil. Un outil ne peut ajouter qu’un purpose :

// inside a tool
let key = ctx.get_secret_for_purpose("weather-api-key", "call the forecast endpoint").await?;

Note : un agent invoqué comme outil franchit un ToolContext, qui ne porte aucune identité propre, donc les accès effectués à l’intérieur de cet agent présentent l’identité de l’agent externe plutôt que celle de l’outil interne. Accordez les autorisations en conséquence.

Audit des accès

SecretAuditSink reçoit un SecretAccessDecision par décision, contenant le résultat, le nom du secret, l’outil, l’utilisateur, l’invocation et la raison — et jamais une valeur secrète. Les autorisations sont également journalisées à info et les refus à warn.

Mise en cache

CachedSecretProvider sert une valeur pour son TTL, puis la récupère à nouveau. Elle est bornée et révocable :

ContrôleComportement
with_max_entries(n)Au plus n noms mis en cache ; le moins récemment utilisé est supprimé quand le cache est plein. La valeur par défaut est 128 ; 0 désactive la mise en cache
invalidate(name)Supprime immédiatement un secret — utilisez ceci lorsqu’un secret est remplacé afin que l’ancienne valeur ne soit pas servie pendant le reste de sa TTL
invalidate_all()Supprime tout
purge_expired()Supprime les entrées expirées sans attendre qu’elles soient relues

Une limite est importante lorsque les noms de secrets sont dérivés de l’entrée : sans elle, le cache peut croître pendant toute la durée de vie du processus.

Ce que le cache garantit et ne garantit pas

Un TTL contrôle ce que le cache retourne, pas la durée pendant laquelle une valeur reste dans la mémoire du processus. Les entrées sont mises à zéro lorsqu’elles expirent, sont évincées ou sont invalidées, ce qui réduit leur présence à peu près à la TTL. Il s’agit d’une réduction, pas d’un effacement — un String a peut-être déjà été réalloué, copié par l’allocateur, échangé sur disque ou capturé dans un dump mémoire. La sortie de débogage du cache est masquée afin qu’une impression de diagnostic ne puisse pas divulguer une valeur.

Important : un SecretProvider nu n’applique aucune politique en propre — tout outil détenant un contexte peut demander n’importe quel nom que les identifiants sous-jacents peuvent lire. Enveloppez-le dans AuthorizingSecretService pour obtenir une frontière par outil, et limitez aussi les identifiants cloud eux-mêmes : une identité IAM par déploiement, avec accès uniquement aux secrets dont ce déploiement a besoin. Important : l’interface du fournisseur n’accepte qu’un nom de secret. Il n’existe pas de permission par outil, d’espace de noms ni d’audit d’accès au niveau de la couche ADK, donc tout outil détenant un contexte peut demander n’importe quel nom que les identifiants sous-jacents peuvent lire. Limitez les identifiants cloud eux-mêmes — une identité IAM par déploiement avec accès à uniquement les secrets dont ce déploiement a besoin — et considérez les journaux d’audit côté fournisseur comme l’enregistrement des accès.

Ce que l’extracteur protège

Configurer un extracteur active l’authentification pour chaque route non publique :

RoutesComportement avec un extracteur configuré
/api/sessions/*, /api/apps/*, artifacts, debug401 sans jeton valide
/api/ui/* — bridge, notifications, resources401 sans jeton valide ; l'utilisateur authentifié remplace tout utilisateur nommé dans le corps de la requête
/api/run*401 sans jeton valide ; l'utilisateur authentifié remplace l'utilisateur fourni
/healthPublic

L’état du pont UI est indexé par (app_name, user_id, session_id) pris dans le corps de la requête, de sorte que l’utilisateur authentifié est substitué à la valeur du corps plutôt que de lui faire confiance. Une ressource UI enregistrée consigne l’utilisateur qui l’a enregistrée ; seul cet utilisateur peut la lire ou la remplacer, et une lecture de la ressource de quelqu’un d’autre renvoie 404 afin que l’existence de URI ne soit pas divulguée.

Avec aucun extracteur configuré, il n’y a pas d’identité authentifiée à lier, donc les routes restent ouvertes et les ressources restent visibles globalement. L’authentification est optionnelle — configurez un extracteur pour tout déploiement qui n’est pas un seul utilisateur de confiance.

Gestion des erreurs

use adk_auth::{AccessDenied, AuthError};
use adk_auth::sso::TokenError;

// RBAC errors
match ac.check("user", &Permission::Tool("admin".into())) {
    Ok(()) => { /* access granted */ }
    Err(AccessDenied { user, permission }) => {
        eprintln!("Denied: {} cannot access {}", user, permission);
    }
}

// SSO errors
match provider.validate(token).await {
    Ok(claims) => { /* token valid */ }
    Err(TokenError::Expired) => { /* token expired */ }
    Err(TokenError::InvalidSignature) => { /* signature invalid */ }
    Err(TokenError::InvalidIssuer { expected, actual }) => { /* wrong issuer */ }
    Err(e) => { /* other error */ }
}

Précédent: ← Évaluation | Suivant: Autorisation des outils →

points de terminaison A2A

ItinéraireAuthentification
GET /.well-known/agent.jsonpublic — les pairs récupèrent la carte avant de détenir un identifiant
POST /a2arequis, lorsqu’un RequestContextExtractor est configuré
POST /a2a/streamobligatoire, lorsqu’un RequestContextExtractor est configuré

Les routes JSON-RPC exécutent le travail de l’agent et des outils, elles portent donc la même couche que les routeurs de session, d’artefact et de débogage. En l’absence d’extracteur configuré, aucune credential n’est demandée et les routes restent ouvertes, donc l’ajout de la porte ne casse pas un déploiement existant.

Important : ces routes étaient auparavant fusionnées à la racine du routeur, en dehors de la couche appliquée à /api. Un déploiement qui authentifiait toutes les autres surfaces de mutation laissait malgré tout n’importe quel client capable d’atteindre le port piloter l’agent et en supporter le coût. Cela s’appliquait à la fois à create_app_with_a2a et à ServerBuilder::build.

A2aServer::builder() se lie par défaut à 127.0.0.1:8080. Appelez bind_addr pour l’exposer, et configurez un extracteur avant de le faire. L’ossature a2a-server générée suit la même règle et lit BIND_HOST pour opter pour une liaison plus large.

Contrôle d’accès - Documentation ADK-Rust | ADK-Rust