Zugriffskontrolle

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

Übersicht

adk-auth bietet rollenbasierte Zugriffskontrolle (RBAC), bereichsbasierte Autorisierung, Audit-Protokollierung und SSO-Unterstützung für ADK-Agenten. Dadurch wird eine sichere, granulare Kontrolle darüber ermöglicht, welche Benutzer auf welche Werkzeuge 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)                          │
└─────────────────────────────────────────────────────────────────┘

Designprinzipien

1. Vorrang der Verweigerung

Wenn eine Rolle sowohl Erlaubnis- als auch Verweigerungsregeln enthält, hat die Verweigerung immer Vorrang:

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 Verweigerungsregeln 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): Überprüft, WER der Benutzer ist (über JWT)
  • Autorisierung (RBAC): Bestimmt, WORAUF der Benutzer 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.1.0"

# For SSO/OAuth support
adk-auth = { version = "2.1.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

Umschließt ein Werkzeug 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 Werkzeuge stapelweise schützen:

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

ScopeGuard

Bereiche für die Autorisierung auf Anfrageebene verwenden, die aus JWT-Ansprüchen oder dem Sitzungsstatus stammen:

use adk_auth::{ContextScopeResolver, ScopeGuard};

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

RBAC + Bereiche kombinieren

RBAC beantwortet die Frage „Darf dieser Benutzer überhaupt auf das Werkzeug zugreifen?“ Bereiche beantworten die Frage „Ist diese konkrete Anfrage 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) oder AzureADProvider::multi_tenant(client).with_allowed_tenants(["tenant-id"])login.microsoftonline.com
OktaOktaProvider::new(domain, client){domain}/oauth2/default
Auth0Auth0Provider::new(domain, audience){domain}/
AllgemeinOidcProvider::from_discovery(issuer, client)Jeder OIDC-Anbieter

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

TokenClaims

Aus validierten JWTs extrahierte Ansprüche:

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 adk-auth-Rollen 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-Anspruch nur, wenn email_verified == true; andernfalls wird auf sub zurückgegriffen.

SsoAccessControl

Kombiniert die Validierung von SSO 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"}

Benutzerdefinierte Audit-Senke

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

Bewährte Sicherheitspraktiken

PraxisBeschreibung
Standardmäßig verweigernNur ausdrücklich benötigte Berechtigungen gewähren
Explizite VerweigerungenVerweigerungsregeln für gefährliche Vorgänge hinzufügen
Alles prüfenProtokollierung zur Einhaltung von Vorschriften aktivieren
Serverseitig validierenJWTs immer auf dem Server validieren
HTTPS verwendenJWKS-Endpunkte erfordern sichere Verbindungen
Schlüssel rotierenDer JWKS-Cache wird jede Stunde automatisch aktualisiert
Lebensdauer von Token begrenzenKurzlebige Zugriffstoken verwenden
Azure-Mandanten beschränkenFür mandantenübergreifende Azure-Apps with_allowed_tenants(...) konfigurieren
E-Mail vor der Identitätszuordnung überprüfenuser_id_from_email() verwendet jetzt sub als Fallback, wenn die E-Mail nicht verifiziert ist
Widerruf einplanenDer Token-Widerruf ist nicht integriert; erzwingen Sie ihn in einem benutzerdefinierten Validator, wenn Sie eine sofortige Sperrung benötigen
Aufwendige Bereichssuchen zwischenspeichernWenn Ihr ScopeResolver externe Systeme aufruft, speichern Sie das Ergebnis pro Anfrage/Sitzung zwischen

Auth-Bridge

Aktiviere auth-bridge, wenn du möchtest, dass adk-auth einen wiederverwendbaren JWT-basierten Anfragen-Extraktor 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 Extraktor validiert das Bearer-Token, ordnet user_id mit ClaimsMapper zu und leitet die JWT-Claims scope / scp an RequestContext.scopes weiter.

Anbieter für Geheimnisse

Tools greifen über ToolContext::get_secret und InvocationContext::get_secret auf Laufzeitgeheimnisse zu. Dahinter befindet sich adk_auth::secrets::SecretProvider, mit Cloud-Implementierungen hinter Feature-Flags:

AnbieterFunktion
AWS Secrets Manageraws-secrets
Azure Key Vaultazure-keyvault
GCP Secret Managergcp-secrets

Füge einen an einen Lauf an, indem du ihn als SecretService umschließt:

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 hält, jedes Geheimnis benennen, und der Anbieter sieht nur diesen Namen – nichts unterscheidet ein Wetter-Tool, das seinen eigenen API-Schlüssel anfordert, von demselben Tool, das eine Zahlungsberechtigung anfordert. AuthorizingSecretService entscheidet pro Tool, bevor der Anbieter 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
Das Tool verfügt über eine Berechtigung, die den Namen abdecktErlaubt
Das Tool verfügt über eine Berechtigung, die den Namen nicht abdecktVerweigert; der Anbieter wird nie aufgerufen
Tool hat keine GenehmigungAbgelehnt
Die Anfrage enthält keine Tool-IdentitätAbgelehnt, sofern grant_untooled es nicht öffnet

Alles wird verweigert, bis es gewährt wird, und eine Verweigerung gibt einen Unauthorized-Fehler zurück. Ein verweigerter Name wird niemals nachgeschlagen und erscheint daher nicht in den zugriffsseitigen Protokollen des Anbieters als versuchter Lesezugriff.

Die Identität wird nicht von einem Tool angegeben. LlmAgent versieht die Anfrage mit dem Namen des weitergeleiteten Tools sowie der App, dem Benutzer, der Sitzung und dem Aufruf, sodass ein Tool nicht die Identität eines anderen Tools präsentieren 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 aufgerufener Agent überschreitet eine ToolContext, die keine eigene Identität besitzt. Daher weisen Zugriffe innerhalb dieses Agents die Identität des äußeren Agents und nicht die des inneren Tools auf. Erteilen Sie Berechtigungen entsprechend.

Zugriffe prüfen

SecretAuditSink erhält pro Entscheidung einen SecretAccessDecision, der das Ergebnis, den Namen des Geheimnisses, das Tool, den Benutzer, den Aufruf und den Grund enthält — niemals jedoch einen Geheimniswert. Erlaubte Zugriffe werden ebenfalls unter info protokolliert, verweigerte Zugriffe unter warn.

Zwischenspeicherung

CachedSecretProvider stellt einen Wert für seine TTL bereit und ruft ihn anschließend erneut ab. Sie ist begrenzt und widerrufbar:

SteuerungVerhalten
with_max_entries(n)Höchstens n Namen werden zwischengespeichert; der am längsten nicht verwendete wird entfernt, wenn der Cache voll ist. Standardmäßig 128; 0 deaktiviert das Zwischenspeichern
invalidate(name)Entfernt ein Secret sofort – verwenden Sie dies, wenn ein Secret rotiert 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

Eine Bindung ist wichtig, wenn geheime Namen aus Eingaben abgeleitet werden: Ohne eine solche kann der Cache während der gesamten Lebensdauer des Prozesses wachsen.

Was der Cache garantiert und was nicht

Ein TTL steuert, was der Cache zurückgibt, nicht, wie lange ein Wert im Prozessspeicher verbleibt. Einträge werden auf null gesetzt, wenn sie ablaufen, entfernt oder ungültig gemacht werden, wodurch sich ihre Verweildauer ungefähr auf TTL verkürzt. Das ist eine Reduzierung, keine Löschung — ein String kann bereits neu zugewiesen, vom Allocator kopiert, auf die Festplatte ausgelagert oder in einem Core-Dump erfasst worden sein. Debug-Ausgaben für den Cache werden redigiert, sodass eine Diagnoseausgabe keinen Wert offenlegen kann.

Wichtig: Ein einfaches SecretProvider wendet keine eigene Richtlinie an — jedes Tool, das einen Kontext besitzt, kann jeden Namen anfordern, den die zugrunde liegenden Anmeldedaten lesen können. Verpacke es in AuthorizingSecretService, um eine Grenze pro Tool zu erhalten, und schränke weiterhin die Cloud-Anmeldedaten selbst ein: eine IAM-Identität pro Bereitstellung mit Zugriff ausschließlich auf die Geheimnisse, die diese Bereitstellung benötigt. Wichtig: Die Provider-Schnittstelle akzeptiert nur einen Namen eines Geheimnisses. Es gibt keine Gewährung pro Tool, keinen Namespace und keine Zugriffsprüfung auf der Ebene ADK. Daher kann jedes Tool, das einen Kontext besitzt, jeden Namen anfordern, den die zugrunde liegenden Anmeldedaten lesen können. Schränke die Cloud-Anmeldedaten selbst ein — eine IAM-Identität pro Bereitstellung mit Zugriff ausschließlich auf die Geheimnisse, die diese Bereitstellung benötigt — und betrachte die Auditprotokolle auf Provider-Seite als Nachweis der Zugriffe.

Was der Extractor schützt

Durch die Konfiguration eines Extractors wird die Authentifizierung für jede nicht öffentliche Route aktiviert:

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 Anforderungstext genannten Benutzer
/api/run*401 ohne gültiges Token; der authentifizierte Benutzer überschreibt den angegebenen Benutzer
/healthÖffentlich

Der Zustand der UI-Bridge wird anhand von (app_name, user_id, session_id) aus dem Anfragerumpf bestimmt, sodass der authentifizierte Benutzer anstelle des Werts aus dem Rumpf eingesetzt wird, statt diesem zu vertrauen. Eine registrierte UI-Ressource speichert den Benutzer, der sie registriert hat; nur dieser Benutzer kann sie lesen oder ersetzen, und das Lesen der Ressource eines anderen Benutzers antwortet mit 404, damit die Existenz von URI nicht offengelegt wird.

Wenn kein Extraktor konfiguriert ist, gibt es keine authentifizierte Identität, an die gebunden werden kann. Daher bleiben die Routen offen und die Ressourcen global sichtbar. Authentifizierung ist optional — konfigurieren Sie für jede Bereitstellung, die nicht für einen einzelnen vertrauenswürdigen Benutzer bestimmt ist, einen Extraktor.

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

Zurück: ← Auswertung | Weiter: Tool-Autorisierung →

A2A Endpunkte

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

Die JSON-RPC-Routen führen Agenten- und Tool-Aufgaben aus und verwenden daher dieselbe Schicht wie die Session-, Artifact- und Debug-Router. Wenn kein Extractor konfiguriert ist, gibt es keine anzufordernde Berechtigung und die Routen bleiben offen; das Hinzufügen des Gates beeinträchtigt daher keine bestehende Bereitstellung.

Wichtig: Diese Routen wurden zuvor am Router-Stamm zusammengeführt, außerhalb der auf /api angewendeten Schicht. Eine Bereitstellung, die jede andere Mutationsschnittstelle authentifizierte, erlaubte weiterhin jedem Client, der den Port erreichen konnte, den Agenten zu steuern und dessen Kosten zu 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. Rufen Sie bind_addr auf, um es bereitzustellen, und konfigurieren Sie zuvor einen Extractor. Das generierte a2a-server-Gerüst folgt derselben Regel und liest BIND_HOST, um eine umfassendere Bindung zu aktivieren.

Zugriffskontrolle - ADK-Rust Dokumentation | ADK-Rust