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
| Anbieter | Konstruktor | Aussteller |
|---|---|---|
GoogleProvider::new(client_id) | accounts.google.com | |
| Azure AD | AzureADProvider::new(tenant, client) oder 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}/ |
| Allgemein | OidcProvider::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
| Praxis | Beschreibung |
|---|---|
| Standardmäßig verweigern | Nur ausdrücklich benötigte Berechtigungen gewähren |
| Explizite Verweigerungen | Verweigerungsregeln für gefährliche Vorgänge hinzufügen |
| Alles prüfen | Protokollierung zur Einhaltung von Vorschriften aktivieren |
| Serverseitig validieren | JWTs immer auf dem Server validieren |
| HTTPS verwenden | JWKS-Endpunkte erfordern sichere Verbindungen |
| Schlüssel rotieren | Der JWKS-Cache wird jede Stunde automatisch aktualisiert |
| Lebensdauer von Token begrenzen | Kurzlebige Zugriffstoken verwenden |
| Azure-Mandanten beschränken | Für mandantenübergreifende Azure-Apps with_allowed_tenants(...) konfigurieren |
| E-Mail vor der Identitätszuordnung überprüfen | user_id_from_email() verwendet jetzt sub als Fallback, wenn die E-Mail nicht verifiziert ist |
| Widerruf einplanen | Der Token-Widerruf ist nicht integriert; erzwingen Sie ihn in einem benutzerdefinierten Validator, wenn Sie eine sofortige Sperrung benötigen |
| Aufwendige Bereichssuchen zwischenspeichern | Wenn 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:
| Anbieter | Funktion |
|---|---|
| AWS Secrets Manager | aws-secrets |
| Azure Key Vault | azure-keyvault |
| GCP Secret Manager | gcp-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),
);
| Regel | Verhalten |
|---|---|
| Das Tool verfügt über eine Berechtigung, die den Namen abdeckt | Erlaubt |
| Das Tool verfügt über eine Berechtigung, die den Namen nicht abdeckt | Verweigert; der Anbieter wird nie aufgerufen |
| Tool hat keine Genehmigung | Abgelehnt |
| Die Anfrage enthält keine Tool-Identität | Abgelehnt, 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:
| Steuerung | Verhalten |
|---|---|
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
SecretProviderwendet keine eigene Richtlinie an — jedes Tool, das einen Kontext besitzt, kann jeden Namen anfordern, den die zugrunde liegenden Anmeldedaten lesen können. Verpacke es inAuthorizingSecretService, 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:
| 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 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
| Route | Authentifizierung |
|---|---|
GET /.well-known/agent.json | öffentlich — Peers rufen die Karte ab, bevor sie über eine Anmeldedaten verfügen |
POST /a2a | erforderlich, wenn ein RequestContextExtractor konfiguriert ist |
POST /a2a/stream | erforderlich, 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
/apiangewendeten 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ürcreate_app_with_a2aals auch fürServerBuilder::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.