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
| Anbieter | Konstruktor | Aussteller |
|---|---|---|
GoogleProvider::new(client_id) | accounts.google.com | |
| Azure AD | AzureADProvider::new(tenant, client) or AzureADProvider::multi_tenant(client).with_allowed_tenants(["tenant-id"]) | login.microsoftonline.com |
| Okta | OktaProvider::new(domain, client) | {domain}/oauth2/default |
| Auth0 | Auth0Provider::new(domain, audience) | {domain}/ |
| Generisch | OidcProvider::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
| Praxis | Beschreibung |
|---|---|
| Standardmäßig verweigern | Nur explizit erforderliche Berechtigungen gewähren |
| Explizite Verweigerungen | Verweigerungsregeln für gefährliche Operationen hinzufügen |
| Alles prüfen | Protokollierung für Compliance aktivieren |
| Serverseitig validieren | JWTs immer auf dem Server validieren |
| HTTPS verwenden | JWKS-Endpunkte erfordern sichere Verbindungen |
| Schlüssel rotieren | JWKS-Cache wird stündlich automatisch aktualisiert |
| Token-Lebensdauer begrenzen | Verwenden Sie kurzlebige Zugriffstoken |
| Azure-Tenants einschränken | Für Azure-Apps mit mehreren Mandanten konfigurieren Sie with_allowed_tenants(...) |
| E-Mail vor der Identitätszuordnung verifizieren | user_id_from_email() fällt jetzt auf sub zurück, wenn die E-Mail nicht verifiziert ist |
| Auf Widerruf vorbereiten | Der Token-Widerruf ist nicht integriert; erzwingen Sie ihn in einem benutzerdefinierten Validator, wenn Sie eine sofortige Sperrung benötigen |
| Teure Scope-Nachschlagen cachen | Wenn 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:
| Anbieter | Funktion |
|---|---|
| AWS Secrets Manager | aws-secrets |
| Azure Key Vault (Schlüsseltresor) | azure-keyvault |
| GCP Secret Manager | gcp-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),
);
| Regel | Verhalten |
|---|---|
| Tool hat eine Berechtigung, die den Namen abdeckt | Erlaubt |
| Tool hat eine Berechtigung, die den Namen nicht abdeckt | Verweigert; der Provider wird nie aufgerufen |
| Das Tool hat keine Berechtigung | Verweigert |
| Die Anfrage trägt keine Tool-Identität | Verweigert, 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:
| Steuerung | Verhalten |
|---|---|
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
SecretProviderwendet 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 inAuthorizingSecretServiceein, 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:
| Routen | Verhalten bei konfiguriertem Extractor |
|---|---|
/api/sessions/*, /api/apps/*, artifacts, debug | 401 ohne gültiges Token |
/api/ui/* — bridge, notifications, resources | 401 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
| Route | Authentifizierung |
|---|---|
GET /.well-known/agent.json | öffentlich — Peers rufen die Karte ab, bevor sie Anmeldedaten besitzen |
POST /a2a | erforderlich, wenn ein RequestContextExtractor konfiguriert ist |
POST /a2a/stream | erforderlich, 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
/apiangewendeten 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ürcreate_app_with_a2aals auch fürServerBuilder::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.