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

ProveedorConstructorEmisor
GoogleGoogleProvider::new(client_id)accounts.google.com
Azure ADAzureADProvider::new(tenant, client) o AzureADProvider::multi_tenant(client).with_allowed_tenants(["tenant-id"])login.microsoftonline.com
OktaOktaProvider::new(domain, client){domain}/oauth2/default
Auth0Auth0Provider::new(domain, audience){domain}/
GenéricoOidcProvider::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ácticaDescripción
Denegar por defectoConcede únicamente los permisos necesarios de forma explícita
Denegaciones explícitasAñade reglas de denegación para operaciones peligrosas
Auditar todoHabilitar el registro para cumplir con la normativa
Validar en el servidorValidar siempre JWTs en el servidor
Usar HTTPSLos endpoints de JWKS requieren conexiones seguras
Rotar las clavesLa caché de JWKS se actualiza automáticamente cada hora
Limitar la duración de los tokensUsa tokens de acceso de corta duración
Restringir los tenants de AzurePara aplicaciones de Azure multiinquilino, configura with_allowed_tenants(...)
Verificar el correo electrónico antes de asignar la identidaduser_id_from_email() ahora recurre a sub cuando el correo electrónico no está verificado
Planificar la revocaciónLa 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 ámbitosSi 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:

ProveedorFuncionalidad
AWS Secrets Manageraws-secrets
Azure Key Vaultazure-keyvault
Administrador de secretos de GCPgcp-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),
);
ReglaComportamiento
La herramienta tiene una concesión que cubre el nombrePermitido
La herramienta tiene una concesión que no cubre el nombreDenegado; el proveedor nunca es llamado
La herramienta no tiene autorizaciónDenegado
La solicitud no incluye la identidad de la herramientaDenegado 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:

ControlComportamiento
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 SecretProvider sin 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 en AuthorizingSecretService para 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:

RutasComportamiento con un extractor configurado
/api/sessions/*, /api/apps/*, artifacts, debug401 sin un token válido
/api/ui/* — bridge, notifications, resources401 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
/healthPú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

RutaAutenticación
GET /.well-known/agent.jsonpública — los pares obtienen la tarjeta antes de disponer de una credencial
POST /a2arequerida, cuando se configura una RequestContextExtractor
POST /a2a/streamobligatorio, 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 a create_app_with_a2a como a ServerBuilder::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.