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

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 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ácticaDescripción
Denegar por defectoConceder solo los permisos explícitamente necesarios
Denegaciones explícitasAñadir reglas de denegación para operaciones peligrosas
Auditar todoHabilitar el registro para cumplimiento
Validar del lado del servidorValidar siempre JWTs en el servidor
Usar HTTPSLos puntos de conexión JWKS requieren conexiones seguras
Rotar clavesLa caché de JWKS se actualiza automáticamente cada hora
Limitar la vida útil del tokenUse tokens de acceso de corta duración
Restringir inquilinos de AzurePara aplicaciones de Azure multiinquilino, configure with_allowed_tenants(...)
Verificar el correo electrónico antes de la asignación de 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 un corte inmediato
Caché de búsquedas de ámbito costosasSi 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:

ProveedorFunción
AWS Secrets Manageraws-secrets
Azure Key Vault (almacén de claves)azure-keyvault
GCP Secret Managergcp-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),
);
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 se llama
La herramienta no tiene permisoDenegado
La solicitud no lleva identidad de herramientaDenegado 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:

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

RutasComportamiento con un extractor configurado
/api/sessions/*, /api/apps/*, artefactos, depuración401 sin un token válido
/api/ui/* — bridge, notificaciones, recursos401 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
/healthPú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

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