Control de acceso
Control de acceso de nivel empresarial para agentes de IA mediante adk-auth.
Descripción general
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 detallado 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 autorización 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 los 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 de forma predeterminada:
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 (mediante 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.1.0"
# For SSO/OAuth support
adk-auth = { version = "2.1.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 varias herramientas por lotes:
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 las declaraciones JWT o del estado de la sesión:
use adk_auth::{ContextScopeResolver, ScopeGuard};
let guard = ScopeGuard::new(ContextScopeResolver);
let protected = guard.protect(my_tool);
Combinación de RBAC + ámbitos
RBAC responde a «¿puede este usuario acceder a la herramienta?». Los ámbitos responden a «¿está autorizada esta solicitud específica en este momento?».
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 OIDC |
AzureADProvider::multi_tenant() acepta cualquier inquilino para la audiencia configurada, a menos que lo restrinja explícitamente con with_allowed_tenants(...).
TokenClaims
Afirmaciones extraídas de JWTs validados:
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
Asigna 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 afirmació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 de auditoría personalizado
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
Prácticas recomendadas de seguridad
| Práctica | Descripción |
|---|---|
| Denegar por defecto | Concede únicamente los permisos necesarios de forma explícita |
| Denegaciones explícitas | Añade reglas de denegación para operaciones peligrosas |
| Auditar todo | Habilitar el registro para cumplir con la normativa |
| Validar en el servidor | Validar siempre JWTs en el servidor |
| Usar HTTPS | Los endpoints de JWKS requieren conexiones seguras |
| Rotar las claves | La caché de JWKS se actualiza automáticamente cada hora |
| Limitar la duración de los tokens | Usa tokens de acceso de corta duración |
| Restringir los tenants de Azure | Para aplicaciones de Azure multiinquilino, configura with_allowed_tenants(...) |
| Verificar el correo electrónico antes de asignar la 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 una interrupción inmediata |
| Almacena en caché las búsquedas costosas de ámbitos | Si tu ScopeResolver llama a sistemas externos, almacena el resultado en caché 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 declaraciones JWT scope / scp a RequestContext.scopes.
Proveedores de secretos
Las herramientas acceden a los secretos del entorno de ejecución mediante ToolContext::get_secret y
InvocationContext::get_secret. Detrás de estos se encuentra adk_auth::secrets::SecretProvider,
con implementaciones en la nube detrás de indicadores de funcionalidad:
| Proveedor | Funcionalidad |
|---|---|
| AWS Secrets Manager | aws-secrets |
| Azure Key Vault | azure-keyvault |
| Administrador de secretos de GCP | gcp-secrets |
Adjunta uno a una ejecución envolviéndola 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 contiene un contexto puede nombrar cualquier secreto, y el proveedor solo ve ese nombre; nada distingue 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 es llamado |
| La herramienta no tiene autorización | Denegado |
| La solicitud no incluye la identidad de la herramienta | Denegado a menos 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 proveedor como una lectura intentada.
La identidad no es algo que una herramienta declare. LlmAgent estampa el nombre de la herramienta despachada en la solicitud, junto con la aplicación, el usuario, la sesión y la invocación, de modo que una herramienta no puede presentar 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 atraviesa un
ToolContext, que no tiene 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 acceso
SecretAuditSink recibe un SecretAccessDecision por decisión, que contiene el resultado, el nombre del secreto, la herramienta, el usuario, la invocación y el motivo, pero nunca un valor secreto. Las concesiones también se registran en info y las denegaciones en warn.
Almacenamiento en caché
CachedSecretProvider sirve un valor durante su TTL y luego vuelve a obtenerlo. Está limitado y puede revocarse:
| Control | Comportamiento |
|---|---|
with_max_entries(n) | Se almacenan en caché como máximo n nombres; el menos usado recientemente se descarta cuando se alcanza el límite. El valor predeterminado es 128; 0 desactiva el almacenamiento en caché |
invalidate(name) | Descarta inmediatamente un secreto; úselo cuando se rote un secreto para que el valor antiguo no se proporcione durante el resto de su TTL |
invalidate_all() | Descarta todo |
purge_expired() | Descarta las entradas expiradas sin esperar a que se vuelvan a leer |
Un límite importa cuando los nombres de los secretos se derivan de la entrada: sin uno, la caché puede crecer durante toda la vida del proceso.
Lo que la caché garantiza y lo que no
Un 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 caducan, se expulsan o se
invalidan, lo que reduce su permanencia aproximadamente a TTL. Eso es una
reducción, no un borrado: un String puede ya haberse reasignado, haber sido copiado por
el asignador, haber sido intercambiado al disco o haber quedado capturado en un volcado
de memoria. La salida de depuración de la caché se redacta para que una impresión
diagnóstica 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 subyacentes puedan leer. Envuélvelo enAuthorizingSecretServicepara obtener un límite por herramienta, y delimita también las credenciales de nube: una identidad de IAM por implementación, con acceso únicamente a los secretos que esa implementación necesita. Importante: la interfaz del proveedor solo acepta un nombre de secreto. No existe ninguna concesión por herramienta, espacio de nombres ni auditoría de acceso en la capa ADK, por lo que cualquier herramienta que tenga un contexto puede solicitar cualquier nombre que las credenciales subyacentes puedan leer. Delimita las credenciales de nube: una identidad de IAM por implementación, con acceso únicamente a los secretos que esa implementación necesita, y considera los registros de auditoría 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/*, artifacts, debug | 401 sin un token válido |
/api/ui/* — bridge, notifications, resources | 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 anula el usuario proporcionado |
/health | Público |
El estado del puente de UI se identifica mediante (app_name, user_id, session_id), obtenido del
cuerpo de la solicitud, por lo que el usuario autenticado sustituye al valor del
cuerpo en lugar de confiar en él. Un recurso de UI registrado registra al usuario
que lo registró; solo ese usuario puede leerlo o reemplazarlo, y la lectura del
recurso de otra persona responde con 404 para que no se revele la existencia de
URI.
Sin un extractor configurado, no hay una identidad autenticada que vincular, por lo que las rutas permanecen abiertas y los recursos siguen siendo visibles globalmente. La autenticación es opcional: configure un extractor para cualquier implementación que no corresponda a un único usuario de confianza.
Gestión 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 →
Endpoints de A2A
| Ruta | Autenticación |
|---|---|
GET /.well-known/agent.json | pública — los pares obtienen la tarjeta antes de disponer de una credencial |
POST /a2a | requerida, cuando se configura una RequestContextExtractor |
POST /a2a/stream | obligatorio, cuando se configura un RequestContextExtractor |
Las rutas de JSON-RPC ejecutan trabajo del agente y de las herramientas, por lo que llevan la misma capa que los enrutadores de sesión, artefactos y depuración. Sin ningún extractor configurado, no hay credenciales que exigir y las rutas permanecen abiertas, por lo que añadir la barrera no interrumpe una implementación existente.
Importante: anteriormente estas rutas se fusionaban en la raíz del enrutador, fuera de la capa aplicada a
/api. Una implementación que autenticaba cualquier otra superficie de mutación aún permitía que cualquier cliente con acceso al puerto controlara el agente e incurriera en su coste. Esto se aplicaba tanto acreate_app_with_a2acomo aServerBuilder::build.
A2aServer::builder() vincula 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 habilitar un enlace más amplio.