Control de Acceso
Control de acceso de nivel empresarial para agentes de IA usando adk-auth.
Resumen
adk-auth proporciona control de acceso basado en roles (RBAC), autorización basada en ámbitos, registro de auditoría y compatibilidad con SSO para agentes ADK. Permite un control seguro y granular sobre qué usuarios pueden acceder a qué herramientas.
Arquitectura
┌─────────────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────────────┘
Principios de diseño
1. Precedencia de denegación
Cuando un rol tiene reglas de अनुमति y denegación, la denegación siempre prevalece:
let role = Role::new("limited")
.allow(Permission::AllTools) // Allow everything...
.deny(Permission::Tool("admin")); // ...except admin
// Result: Can access any tool EXCEPT "admin"
2. Unión de múltiples roles
Los usuarios con varios roles obtienen la unión de permisos, pero las reglas de denegación de cualquier rol siguen aplicándose:
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. Explícito sobre implícito
Los permisos son explícitos: no se concede acceso por defecto:
let role = Role::new("empty");
// This role grants NO permissions
ac.check("user", &Permission::Tool("anything")); // → Denied
4. Separación de autenticación y autorización
- Autenticación (SSO): Verifica QUIÉN es el usuario (a través de JWT)
- Autorización (RBAC): Determina A QUÉ puede acceder
// 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?;
Instalación
[dependencies]
adk-auth = "2.0.0"
# For SSO/OAuth support
adk-auth = { version = "2.0.0", features = ["sso"] }
Componentes principales
Permiso
pub enum Permission {
Tool(String), // Specific tool by name
AllTools, // Wildcard: all tools
Agent(String), // Specific agent by name
AllAgents, // Wildcard: all agents
}
Rol
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
Envuelve una herramienta con comprobación automática de permisos:
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
Protege en lote múltiples herramientas:
let middleware = AuthMiddleware::new(ac);
let protected_tools = middleware.protect_all(tools);
ScopeGuard
Usa ámbitos para la autorización a nivel de solicitud que proviene de JWT o del estado de la sesión:
use adk_auth::{ContextScopeResolver, ScopeGuard};
let guard = ScopeGuard::new(ContextScopeResolver);
let protected = guard.protect(my_tool);
Combinando RBAC + ámbitos
RBAC responde a "¿puede este usuario acceder a la herramienta en absoluto?" Los ámbitos responden a "¿está autorizada esta solicitud específica ahora mismo?"
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));
Integración de SSO
Proveedores compatibles
| Proveedor | Constructor | Emisor |
|---|---|---|
GoogleProvider::new(client_id) | accounts.google.com | |
| Azure AD | AzureADProvider::new(tenant, client) o 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}/ |
| Genérico | OidcProvider::from_discovery(issuer, client) | Cualquier proveedor de OIDC |
AzureADProvider::multi_tenant() acepta cualquier tenant para la audiencia configurada, a menos que lo restrinjas explícitamente con with_allowed_tenants(...).
TokenClaims
Reclamaciones extraídas de JWTs validadas:
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
Mapea los grupos de IdP a roles de 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() solo usa la reclamación de correo electrónico cuando email_verified == true; de lo contrario, recurre a sub.
SsoAccessControl
Combina la validación de SSO con RBAC en una sola llamada:
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?;
Registro de auditoría
FileAuditSink
let audit = FileAuditSink::new("/var/log/adk/audit.jsonl")?;
let middleware = AuthMiddleware::with_audit(ac, audit);
Formato de salida (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"}
Destino personalizado de auditoría
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(())
}
}
Ejemplos
cargo check -p adk-auth
cargo check -p adk-auth --features sso
Mejores prácticas de seguridad
| Práctica | Descripción |
|---|---|
| Denegar por defecto | Conceder solo los permisos explícitamente necesarios |
| Denegaciones explícitas | Añadir reglas de denegación para operaciones peligrosas |
| Auditar todo | Habilitar el registro para cumplimiento |
| Validar del lado del servidor | Validar siempre JWTs en el servidor |
| Usar HTTPS | Los puntos de conexión JWKS requieren conexiones seguras |
| Rotar claves | La caché de JWKS se actualiza automáticamente cada hora |
| Limitar la vida útil del token | Use tokens de acceso de corta duración |
| Restringir inquilinos de Azure | Para aplicaciones de Azure multiinquilino, configure with_allowed_tenants(...) |
| Verificar el correo electrónico antes de la asignación de identidad | user_id_from_email() ahora recurre a sub cuando el correo electrónico no está verificado |
| Planificar la revocación | La revocación de tokens no está integrada; aplícala en un validador personalizado si necesitas un corte inmediato |
| Caché de búsquedas de ámbito costosas | Si tu ScopeResolver llama a sistemas externos, almacena en caché el resultado por solicitud/sesión |
Puente de autenticación
Habilita auth-bridge cuando quieras que adk-auth proporcione un extractor de solicitudes reutilizable basado en JWT para 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()?;
El extractor valida el token Bearer, asigna user_id con ClaimsMapper y reenvía las reclamaciones JWT scope / scp hacia RequestContext.scopes.
Proveedores de secretos
Las herramientas acceden a los secretos en tiempo de ejecución a través de ToolContext::get_secret y
InvocationContext::get_secret. Detrás de ellos está adk_auth::secrets::SecretProvider,
con implementaciones en la nube detrás de marcas de funcionalidad:
| Proveedor | Función |
|---|---|
| AWS Secrets Manager | aws-secrets |
| Azure Key Vault (almacén de claves) | azure-keyvault |
| GCP Secret Manager | gcp-secrets |
Adjunte uno a una ejecución envolviéndolo como 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));
Autorización por herramienta
De forma predeterminada, una herramienta que tiene un contexto puede nombrar cualquier secreto, y el proveedor solo ve
ese nombre — no hay nada que distinga a una herramienta meteorológica que solicita su propia clave API de la
misma herramienta que solicita una credencial de pago. AuthorizingSecretService decide por herramienta
antes de consultar al proveedor:
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),
);
| Regla | Comportamiento |
|---|---|
| La herramienta tiene una concesión que cubre el nombre | Permitido |
| La herramienta tiene una concesión que no cubre el nombre | Denegado; el proveedor nunca se llama |
| La herramienta no tiene permiso | Denegado |
| La solicitud no lleva identidad de herramienta | Denegado salvo que grant_untooled lo abra |
Todo se deniega hasta que se concede, y una denegación devuelve un error Unauthorized. Un nombre denegado nunca se busca, por lo que no aparece en los registros de acceso del lado del proveedor como un intento de lectura.
La identidad no es algo que una herramienta afirme. LlmAgent imprime el nombre de la herramienta despachada en la solicitud, junto con la app, el usuario, la sesión y la invocación, de modo que una herramienta no puede presentarse con la identidad de otra herramienta. Una herramienta solo puede añadir un propósito:
// inside a tool
let key = ctx.get_secret_for_purpose("weather-api-key", "call the forecast endpoint").await?;
Nota: un agente invocado como herramienta cruza un
ToolContext, que no lleva identidad propia, por lo que los accesos realizados dentro de ese agente presentan la identidad del agente externo en lugar de la de la herramienta interna. Concede en consecuencia.
Auditoría de accesos
SecretAuditSink recibe un SecretAccessDecision por decisión, que lleva el resultado, el nombre del secreto, la herramienta, el usuario, la invocación y el motivo —y nunca un valor secreto. Los permisos concedidos también se registran en info y las denegaciones en warn.
Caché
CachedSecretProvider sirve un valor para su TTL y luego vuelve a obtenerlo. Está limitado y es revocable:
| Control | Comportamiento |
|---|---|
with_max_entries(n) | Como máximo se almacenan en caché n nombres; el menos recientemente usado se descarta cuando está lleno. El valor predeterminado es 128; 0 deshabilita la caché |
invalidate(name) | Descarta un secreto de inmediato — use esto cuando se rote un secreto para que el valor antiguo no se sirva durante el resto de su TTL |
invalidate_all() | Elimina todo |
purge_expired() | Elimina las entradas caducadas sin esperar a que se vuelvan a leer |
Un límite importa cuando los nombres secretos se derivan de la entrada: sin uno, la caché puede crecer durante toda la vida del proceso.
Lo que la caché garantiza y no garantiza
Una TTL controla lo que la caché devuelve, no cuánto tiempo permanece un valor en la memoria del proceso. Las entradas se ponen a cero cuando expiran, se expulsan o se invalidan, lo que reduce la permanencia a aproximadamente la TTL. Eso es una reducción, no un borrado — una String puede ya haberse reasignado, copiada por el asignador, intercambiada a disco o capturada en un volcado de núcleo. La salida de depuración de la caché se redacta para que una impresión de diagnóstico no pueda filtrar un valor.
Importante: un
SecretProvidersin más no aplica ninguna política propia — cualquier herramienta que tenga un contexto puede solicitar cualquier nombre que las credenciales de respaldo puedan leer. Envuélvalo enAuthorizingSecretServicepara obtener un límite por herramienta, y aun así delimite las credenciales de la nube en sí: una identidad IAM por despliegue con acceso solo a los secretos que ese despliegue necesita. Importante: la interfaz del proveedor solo toma un secreto nombre. No hay concesión por herramienta, espacio de nombres ni auditoría de acceso en la capa ADK, así que cualquier herramienta que tenga un contexto puede solicitar cualquier nombre que las credenciales de respaldo puedan leer. Delimite las credenciales de la nube en sí — una identidad IAM por despliegue con acceso solo a los secretos que ese despliegue necesita — y trate los registros de auditoría del lado del proveedor como el registro de acceso.
Lo que protege el extractor
Configurar un extractor activa la autenticación para cada ruta no pública:
| Rutas | Comportamiento con un extractor configurado |
|---|---|
/api/sessions/*, /api/apps/*, artefactos, depuración | 401 sin un token válido |
/api/ui/* — bridge, notificaciones, recursos | 401 sin un token válido; el usuario autenticado reemplaza a cualquier usuario nombrado en el cuerpo de la solicitud |
/api/run* | 401 sin un token válido; el usuario autenticado reemplaza al usuario proporcionado |
/health | Público |
El estado del puente de UI se indexa por (app_name, user_id, session_id) tomado del cuerpo de la solicitud, de modo que el usuario autenticado se sustituye por el valor del cuerpo en lugar de confiar en él. Un recurso de UI registrado guarda el usuario que lo registró; solo ese usuario puede leerlo o reemplazarlo, y una lectura del recurso de otra persona responde 404, por lo que no se revela la existencia de URI.
Sin ningún extractor configurado, no hay una identidad autenticada a la que vincular, así que las rutas permanecen abiertas y los recursos siguen siendo visibles globalmente. La autenticación es opcional: configura un extractor para cualquier despliegue que no sea un único usuario de confianza.
Manejo de errores
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 */ }
}
Anterior: ← Evaluación | Siguiente: Autorización de herramientas →
A2A endpoints
| Ruta | Autenticación |
|---|---|
GET /.well-known/agent.json | público — los pares obtienen la tarjeta antes de tener una credencial |
POST /a2a | requerido, cuando se configura un RequestContextExtractor |
POST /a2a/stream | obligatorio, cuando se configura un RequestContextExtractor |
Las rutas de JSON-RPC ejecutan el trabajo del agente y de las herramientas, por lo que llevan la misma capa que los routers de sesión, artefactos y depuración. Sin un extractor configurado, no hay ninguna credencial que exigir y las rutas permanecen abiertas, por lo que añadir la compuerta no rompe una implementación existente.
Importante: estas rutas se fusionaban anteriormente en la raíz del router, fuera de la capa aplicada a
/api. Una implementación que autenticaba cada otra superficie de mutación seguía permitiendo que cualquier cliente que pudiera الوصول al puerto controlara el agente e incurriera en su coste. Esto se aplicaba tanto acreate_app_with_a2acomo aServerBuilder::build.
A2aServer::builder() enlaza 127.0.0.1:8080 de forma predeterminada. Llama a bind_addr para exponerlo, y
configura un extractor antes de hacerlo. El esqueleto a2a-server generado sigue la misma regla
y lee BIND_HOST para optar por un enlace más amplio.