Zugriffskontrolle

Zugriffskontrolle auf Enterprise-Niveau für KI-Agenten mit adk-auth.

Überblick

adk-auth bietet rollenbasierte Zugriffskontrolle (RBAC), berechtigungsbereichsbasierte Autorisierung, Audit-Logging und Unterstützung für SSO für ADK-Agenten. Es ermöglicht eine sichere, fein abgestufte Kontrolle darüber, welche Benutzer auf welche Tools zugreifen können.

Architektur

┌─────────────────────────────────────────────────────────────────┐
│                        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)                          │
└─────────────────────────────────────────────────────────────────┘

Design-Prinzipien

1. Vorrang von Verweigerung

Wenn eine Rolle sowohl Allow- als auch Deny-Regeln hat, gewinnt deny immer:

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

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

2. Vereinigung mehrerer Rollen

Benutzer mit mehreren Rollen erhalten die Vereinigung der Berechtigungen, aber Deny-Regeln aus jeder Rolle gelten weiterhin:

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. Explizit statt implizit

Berechtigungen werden explizit vergeben - standardmäßig wird kein Zugriff gewährt:

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

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

4. Trennung von Authentifizierung und Autorisierung

  • Authentifizierung (SSO): Verifiziert, WER der Benutzer ist (über JWT)
  • Autorisierung (RBAC): Bestimmt, WORAUF er zugreifen kann
// 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"] }

Kernkomponenten

Berechtigung

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

Rolle

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

Umhüllt ein Tool mit automatischer Berechtigungsprüfung:

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

Mehrere Tools stapelweise schützen:

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

ScopeGuard

Verwende Berechtigungsbereiche für Autorisierung auf Request-Ebene, die aus JWT-Claims oder dem Sitzungszustand stammen:

use adk_auth::{ContextScopeResolver, ScopeGuard};

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

Kombination von RBAC + Berechtigungsbereichen

RBAC beantwortet "darf dieser Benutzer überhaupt auf das Tool zugreifen?" Berechtigungsbereiche beantworten "ist diese spezifische Anfrage gerade jetzt autorisiert?"

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));

SSO-Integration

Unterstützte Anbieter

AnbieterKonstruktorAussteller
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}/
GenerischOidcProvider::from_discovery(issuer, client)Jeder OIDC-Anbieter

AzureADProvider::multi_tenant() akzeptiert jeden Tenant für die konfigurierte Zielgruppe, sofern Sie dies nicht ausdrücklich mit with_allowed_tenants(...) einschränken.

TokenClaims

Aus validierten JWTs extrahierte Claims:

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

Ordnet IdP-Gruppen Rollen in adk-auth zu:

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

user_id_from_email() verwendet den E-Mail-Claim nur, wenn email_verified == true; andernfalls greift es auf sub zurück.

SsoAccessControl

Kombiniert SSO-Validierung mit RBAC in einem Aufruf:

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?;

Audit-Protokollierung

FileAuditSink

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

Ausgabeformat (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"}

Benutzerdefinierter Audit-Sink

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(())
    }
}

Beispiele

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

Sicherheits-Best Practices

PraxisBeschreibung
Standardmäßig verweigernNur explizit erforderliche Berechtigungen gewähren
Explizite VerweigerungenVerweigerungsregeln für gefährliche Operationen hinzufügen
Alles prüfenProtokollierung für Compliance aktivieren
Serverseitig validierenJWTs immer auf dem Server validieren
HTTPS verwendenJWKS-Endpunkte erfordern sichere Verbindungen
Schlüssel rotierenJWKS-Cache wird stündlich automatisch aktualisiert
Token-Lebensdauer begrenzenVerwenden Sie kurzlebige Zugriffstoken
Azure-Tenants einschränkenFür Azure-Apps mit mehreren Mandanten konfigurieren Sie with_allowed_tenants(...)
E-Mail vor der Identitätszuordnung verifizierenuser_id_from_email() fällt jetzt auf sub zurück, wenn die E-Mail nicht verifiziert ist
Auf Widerruf vorbereitenDer Token-Widerruf ist nicht integriert; erzwingen Sie ihn in einem benutzerdefinierten Validator, wenn Sie eine sofortige Sperrung benötigen
Teure Scope-Nachschlagen cachenWenn Ihr ScopeResolver externe Systeme aufruft, cachen Sie das Ergebnis pro Anfrage/Sitzung

Auth-Bridge

Aktiviere auth-bridge, wenn du möchtest, dass adk-auth einen wiederverwendbaren JWT-basierten Request-Extractor für adk-server bereitstellt:

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()?;

Der Extractor validiert das Bearer-Token, ordnet user_id mit ClaimsMapper zu und leitet JWT scope / scp-Claims an RequestContext.scopes weiter.

Secret-Provider

Tools greifen zur Laufzeit über ToolContext::get_secret und InvocationContext::get_secret auf Secrets zu. Dahinter steht adk_auth::secrets::SecretProvider, mit Cloud-Implementierungen hinter Feature-Flags:

AnbieterFunktion
AWS Secrets Manageraws-secrets
Azure Key Vault (Schlüsseltresor)azure-keyvault
GCP Secret Managergcp-secrets

Binden Sie einen Dienst in einen Lauf ein, indem Sie ihn als SecretService kapseln:

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));

Autorisierung pro Tool

Standardmäßig kann ein Tool, das einen Kontext besitzt, jeden Secret-Namen angeben, und der Provider sieht nur diesen Namen — nichts unterscheidet ein Wetter-Tool, das nach seinem eigenen API-Schlüssel fragt, von demselben Tool, das nach einer Zahlungsanmeldedaten fragt. AuthorizingSecretService entscheidet pro Tool, bevor der Provider konsultiert wird:

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),
);
RegelVerhalten
Tool hat eine Berechtigung, die den Namen abdecktErlaubt
Tool hat eine Berechtigung, die den Namen nicht abdecktVerweigert; der Provider wird nie aufgerufen
Das Tool hat keine BerechtigungVerweigert
Die Anfrage trägt keine Tool-IdentitätVerweigert, sofern nicht grant_untooled sie öffnet

Alles ist verweigert, bis es gewährt wird, und eine Verweigerung gibt einen Unauthorized-Fehler zurück. Ein verweigerter Name wird niemals nachgeschlagen, daher erscheint er nicht in den Zugriffsprotokollen auf Anbieterseite als versuchter Lesezugriff.

Die Identität ist nichts, was ein Tool behauptet. LlmAgent prägt den Namen des ausgelieferten Tools auf die Anfrage, zusammen mit der App, dem Benutzer, der Session und dem Aufruf, sodass ein Tool nicht die Identität eines anderen Tools vorgeben kann. Ein Tool kann nur einen Zweck hinzufügen:

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

Hinweis: Ein als Tool aufgerufenes Agent überschreitet eine ToolContext, die keine eigene Identität trägt, sodass Zugriffe innerhalb dieses Agents die Identität des äußeren Agents statt die des inneren Tools anzeigen. Entsprechend gewähren.

Zugriff prüfen

SecretAuditSink erhält eine SecretAccessDecision pro Entscheidung, die das Ergebnis, den geheimen Namen, das Tool, den Benutzer, den Aufruf und den Grund enthält — und niemals einen geheimen Wert. Gewährungen werden außerdem bei info protokolliert und Verweigerungen bei warn.

Caching

CachedSecretProvider liefert einen Wert für sein TTL aus und ruft ihn dann erneut ab. Es ist begrenzt und widerrufbar:

SteuerungVerhalten
with_max_entries(n)Es werden höchstens n Namen zwischengespeichert; der am längsten nicht verwendete wird verworfen, wenn der Speicher voll ist. Standardwert ist 128; 0 deaktiviert das Caching
invalidate(name)Verwirft sofort ein Geheimnis — verwenden Sie dies, wenn ein Geheimnis ausgetauscht wird, damit der alte Wert für den Rest seiner TTL nicht ausgeliefert wird
invalidate_all()Verwirft alles
purge_expired()Verwirft abgelaufene Einträge, ohne darauf zu warten, dass sie erneut gelesen werden

Ein Bound ist relevant, wenn geheime Namen aus Eingaben abgeleitet werden: Ohne einen Bound kann der Cache über die gesamte Lebensdauer des Prozesses anwachsen.

Was der Cache garantiert und was nicht

Ein TTL steuert, was der Cache zurückgibt, nicht wie lange ein Wert im Prozessspeicher bleibt. Einträge werden beim Ablauf ihrer Gültigkeit, beim Verdrängen oder bei der Ungültigerklärung mit Nullen überschrieben, was die Verweildauer ungefähr auf die TTL verkürzt. Das ist eine Reduktion, keine Löschung — ein String kann bereits neu zugewiesen, vom Allokator kopiert, auf die Festplatte ausgelagert oder in einem Core-Dump erfasst worden sein. Die Debug-Ausgabe für den Cache wird geschwärzt, damit ein Diagnose-Print keinen Wert preisgeben kann.

Wichtig: Ein nackter SecretProvider wendet keine eigene Richtlinie an — jedes Tool mit einem Kontext kann jeden Namen anfordern, den die zugrunde liegenden Anmeldedaten lesen können. Um eine pro-Tool-Grenze zu erhalten, binde ihn in AuthorizingSecretService ein, und begrenze weiterhin auch die Cloud-Anmeldedaten selbst: eine IAM-Identität pro Bereitstellung mit Zugriff nur auf die Secrets, die diese Bereitstellung benötigt. Wichtig: Die Provider-Schnittstelle nimmt nur einen Secret-Namen an. Auf der ADK-Ebene gibt es keine pro-Tool-Freigabe, keinen Namespace und kein Zugriffs-Audit, sodass jedes Tool mit einem Kontext jeden Namen anfordern kann, den die zugrunde liegenden Anmeldedaten lesen können. Begrenze die Cloud- Anmeldedaten selbst — eine IAM-Identität pro Bereitstellung mit Zugriff nur auf die Secrets, die diese Bereitstellung benötigt — und betrachte die Audit-Logs auf Provider-Seite als das Protokoll des Zugriffs.

Was der Extraktor schützt

Das Konfigurieren eines Extraktors schaltet die Authentifizierung für jede nicht öffentliche Route ein:

RoutenVerhalten bei konfiguriertem Extractor
/api/sessions/*, /api/apps/*, artifacts, debug401 ohne gültiges Token
/api/ui/* — bridge, notifications, resources401 ohne gültiges Token; der authentifizierte Benutzer ersetzt jeden im Anfragekörper genannten Benutzer
/api/run*401 ohne gültigen Token; der authentifizierte Benutzer überschreibt den angegebenen Benutzer
/healthÖffentlich

Der Zustand des UI-Bridge wird nach (app_name, user_id, session_id) aus dem Request-Body geschlüsselt, sodass der authentifizierte Benutzer anstelle des Body-Werts eingesetzt wird und diesem nicht vertraut wird. Eine registrierte UI-Ressource speichert den Benutzer, der sie registriert hat; nur dieser Benutzer kann sie lesen oder ersetzen, und ein Lesezugriff auf die Ressource eines anderen antwortet mit 404, sodass die Existenz von URI nicht offengelegt wird.

Ohne konfigurierten Extractor gibt es keine authentifizierte Identität, an die gebunden werden kann, daher bleiben Routen offen und Ressourcen bleiben global sichtbar. Authentifizierung ist opt-in — konfigurieren Sie einen Extractor für jede Bereitstellung, die nicht aus einem einzelnen vertrauenswürdigen Benutzer besteht.

Fehlerbehandlung

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 */ }
}

Vorherige: ← Evaluation | Nächste: Tool-Autorisierung →

A2A Endpunkte

RouteAuthentifizierung
GET /.well-known/agent.jsonöffentlich — Peers rufen die Karte ab, bevor sie Anmeldedaten besitzen
POST /a2aerforderlich, wenn ein RequestContextExtractor konfiguriert ist
POST /a2a/streamerforderlich, wenn ein RequestContextExtractor konfiguriert ist

Die Routen JSON-RPC führen Agent- und Tool-Arbeit aus, daher tragen sie dieselbe Ebene wie die Session-, Artifact- und Debug-Router. Ohne konfigurierte Extraktion gibt es keine Anmeldeinformationen, die angefordert werden könnten, und die Routen bleiben offen, sodass das Hinzufügen des Gates eine bestehende Bereitstellung nicht bricht.

Wichtig: Diese Routen wurden zuvor am Router-Root zusammengeführt, außerhalb der auf /api angewendeten Ebene. Eine Bereitstellung, die jede andere Mutationsoberfläche authentifizierte, ließ dennoch jeden Client, der den Port erreichen konnte, den Agenten steuern und dessen Kosten verursachen. Dies galt sowohl für create_app_with_a2a als auch für ServerBuilder::build.

A2aServer::builder() bindet standardmäßig an 127.0.0.1:8080. Rufe bind_addr auf, um es freizugeben, und konfiguriere vorher einen Extraktor. Das generierte a2a-server-Gerüst folgt derselben Regel und liest BIND_HOST, um einen breiteren Bind zu aktivieren.

Zugriffskontrolle - ADK-Rust Dokumentation | ADK-Rust