Controle de Acesso

Controle de acesso de nível empresarial para agentes de IA usando adk-auth.

Visão geral

adk-auth fornece controle de acesso baseado em funções (RBAC), autorização baseada em escopos, registro de auditoria e suporte a SSO para agentes ADK. Ele permite um controle seguro e detalhado sobre quais usuários podem acessar quais ferramentas.

Arquitetura

┌─────────────────────────────────────────────────────────────────┐
│                        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)                          │
└─────────────────────────────────────────────────────────────────┘

Princípios de design

1. Precedência da negação

Quando uma função possui regras de permissão e negação, a negação sempre 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ão de múltiplas funções

Usuários com várias funções recebem a união das permissões, mas as regras de negação de qualquer função ainda se aplicam:

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

As permissões são explícitas — nenhum acesso é concedido por padrão:

let role = Role::new("empty");
// This role grants NO permissions

ac.check("user", &Permission::Tool("anything")); // → Denied

4. Separação entre autenticação e autorização

  • Autenticação (SSO): verifica QUEM é o usuário (por meio de JWT)
  • Autorização (RBAC): determina O QUE ele pode acessar
// 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?;

Instalação

[dependencies]
adk-auth = "2.1.0"

# For SSO/OAuth support
adk-auth = { version = "2.1.0", features = ["sso"] }

Componentes principais

Permissão

pub enum Permission {
    Tool(String),     // Specific tool by name
    AllTools,         // Wildcard: all tools
    Agent(String),    // Specific agent by name  
    AllAgents,        // Wildcard: all agents
}

Função

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

Encapsula uma ferramenta com verificação automática de permissões:

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

Proteja várias ferramentas em lote:

let middleware = AuthMiddleware::new(ac);
let protected_tools = middleware.protect_all(tools);

ScopeGuard

Use escopos para autorização no nível da solicitação proveniente de declarações JWT ou do estado da sessão:

use adk_auth::{ContextScopeResolver, ScopeGuard};

let guard = ScopeGuard::new(ContextScopeResolver);
let protected = guard.protect(my_tool);

Combinando RBAC + escopos

RBAC responde à pergunta “este usuário pode acessar a ferramenta de alguma forma?”. Os escopos respondem à pergunta “esta solicitação específica está autorizada neste 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));

Integração com SSO

Provedores compatíveis

ProvedorConstrutorEmissor
GoogleGoogleProvider::new(client_id)accounts.google.com
Azure ADAzureADProvider::new(tenant, client) ou 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)Qualquer provedor OIDC

AzureADProvider::multi_tenant() aceita qualquer locatário para o público configurado, a menos que você o restrinja explicitamente com with_allowed_tenants(...).

TokenClaims

Declarações 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

Mapeia grupos de IdP para funções 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() usa a declaração de e-mail somente quando email_verified == true; caso contrário, recorre a sub.

SsoAccessControl

Combina a validação de SSO com RBAC em uma única chamada:

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 auditoria

FileAuditSink

let audit = FileAuditSink::new("/var/log/adk/audit.jsonl")?;
let middleware = AuthMiddleware::with_audit(ac, audit);

Formato de saída (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 auditoria 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(())
    }
}

Exemplos

cargo check -p adk-auth
cargo check -p adk-auth --features sso

Práticas recomendadas de segurança

PráticaDescrição
Negar por padrãoConceda apenas as permissões explicitamente necessárias
Negativas explícitasAdicione regras de negação para operações perigosas
Audite tudoHabilite o registro de logs para conformidade
Valide no servidorSempre valide JWTs no servidor
Use HTTPSOs endpoints JWKS exigem conexões seguras
Faça a rotação das chavesO cache JWKS é atualizado automaticamente a cada hora
Limitar o tempo de vida do tokenUse tokens de acesso de curta duração
Restringir os tenants do AzurePara aplicativos do Azure multilocatários, configure with_allowed_tenants(...)
Verificar o e-mail antes do mapeamento de identidadeuser_id_from_email() agora usa sub como alternativa quando o e-mail não é verificado
Planejar a revogaçãoA revogação de tokens não é integrada; aplique-a em um validador personalizado se precisar de interrupção imediata
Armazene em cache as consultas dispendiosas de escopoSe o seu ScopeResolver chamar sistemas externos, armazene o resultado em cache por solicitação/sessão

Ponte de autenticação

Habilite auth-bridge quando quiser que adk-auth forneça um extrator de solicitações reutilizável baseado em 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()?;

O extrator valida o token Bearer, mapeia user_id com ClaimsMapper e encaminha as declarações JWT scope / scp para RequestContext.scopes.

Provedores de segredos

As ferramentas acessam os segredos de tempo de execução por meio de ToolContext::get_secret e InvocationContext::get_secret. Por trás deles está adk_auth::secrets::SecretProvider, com implementações para a nuvem disponíveis por meio de flags de recursos:

ProvedorRecurso
AWS Secrets Manageraws-secrets
Azure Key Vaultazure-keyvault
Gerenciador de segredos do GCPgcp-secrets

Anexe um a uma execução envolvendo-a como 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));

Autorização por ferramenta

Por padrão, uma ferramenta que mantém um contexto pode nomear qualquer segredo, e o provedor vê apenas esse nome — nada distingue uma ferramenta de clima solicitando sua própria chave API da mesma ferramenta solicitando uma credencial de pagamento. AuthorizingSecretService decide por ferramenta antes que o provedor seja consultado:

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),
);
RegraComportamento
A ferramenta tem uma concessão que abrange o nomePermitido
A ferramenta tem uma concessão que não abrange o nomeNegado; o provedor nunca é chamado
A ferramenta não tem concessãoNegado
A solicitação não contém a identidade da ferramentaNegado, a menos que grant_untooled a abra

Tudo é negado até ser concedido, e uma negação retorna um erro Unauthorized. Um nome negado nunca é consultado, portanto não aparece nos logs de acesso do lado do provedor como uma leitura tentada.

A identidade não é algo que uma ferramenta declara. LlmAgent registra o nome da ferramenta despachada na solicitação, junto com o aplicativo, o usuário, a sessão e a invocação, portanto uma ferramenta não pode apresentar a identidade de outra ferramenta. Uma ferramenta pode adicionar apenas um propósito:

// inside a tool
let key = ctx.get_secret_for_purpose("weather-api-key", "call the forecast endpoint").await?;

Observação: um agente invocado como uma ferramenta atravessa um ToolContext, que não carrega uma identidade própria, portanto os acessos realizados dentro desse agente apresentam a identidade do agente externo, e não a da ferramenta interna. Conceda as permissões de acordo.

Auditoria de acessos

SecretAuditSink recebe um SecretAccessDecision por decisão, contendo o resultado, o nome do segredo, a ferramenta, o usuário, a invocação e o motivo — e nunca um valor secreto. As autorizações também são registradas em info e as negações em warn.

Cache

CachedSecretProvider fornece um valor durante seu TTL e, em seguida, busca-o novamente. Ele é limitado e revogável:

ControleComportamento
with_max_entries(n)No máximo n nomes armazenados em cache; o menos usado recentemente é descartado quando o limite é atingido. O padrão é 128; 0 desativa o armazenamento em cache
invalidate(name)Descarta um segredo imediatamente — use quando um segredo for alternado para que o valor antigo não seja fornecido durante o restante de seu TTL
invalidate_all()Descarta tudo
purge_expired()Descarta entradas expiradas sem esperar que sejam lidas novamente

Um limite é importante quando os nomes dos segredos são derivados da entrada: sem um, o cache pode crescer durante toda a vida útil do processo.

O que o cache garante e o que não garante

Um TTL controla o que o cache retorna, não por quanto tempo um valor permanece na memória do processo. As entradas são zeradas quando expiram, são removidas ou invalidadas, reduzindo a permanência para aproximadamente TTL. Isso é uma redução, não uma eliminação — um String pode já ter sido realocado, copiado pelo alocador, gravado em disco ou capturado em um despejo de memória. A saída de depuração do cache é redigida para que uma impressão de diagnóstico não possa vazar um valor.

Importante: um SecretProvider isolado não aplica nenhuma política própria — qualquer ferramenta que mantenha um contexto pode solicitar qualquer nome que as credenciais subjacentes possam ler. Envolva-o em AuthorizingSecretService para obter um limite por ferramenta e ainda restrinja as próprias credenciais da nuvem: uma identidade do IAM por implantação, com acesso apenas aos segredos de que essa implantação precisa. Importante: a interface do provedor recebe apenas um nome de segredo. Não há concessão por ferramenta, namespace ou auditoria de acesso na camada ADK, portanto qualquer ferramenta que mantenha um contexto pode solicitar qualquer nome que as credenciais subjacentes possam ler. Restrinja as próprias credenciais da nuvem — uma identidade do IAM por implantação, com acesso apenas aos segredos de que essa implantação precisa — e trate os logs de auditoria do provedor como o registro de acesso.

O que o extrator protege

Configurar um extrator ativa a autenticação para todas as rotas não públicas:

RotasComportamento com um extrator configurado
/api/sessions/*, /api/apps/*, artifacts, debug401 sem um token válido
/api/ui/* — bridge, notifications, resources401 sem um token válido; o usuário autenticado substitui qualquer usuário nomeado no corpo da solicitação
/api/run*401 sem um token válido; o usuário autenticado substitui o usuário fornecido
/healthPúblico

O estado da ponte de UI é indexado por (app_name, user_id, session_id) obtido do corpo da requisição, portanto o usuário autenticado substitui o valor do corpo, em vez de ser confiado. Um recurso de UI registrado armazena o usuário que o registrou; somente esse usuário pode lê-lo ou substituí-lo, e uma leitura do recurso de outra pessoa responde com 404, para que a existência de URI não seja divulgada.

Sem um extrator configurado, não há uma identidade autenticada para associar, portanto as rotas permanecem abertas e os recursos continuam globalmente visíveis. A autenticação é opcional — configure um extrator para qualquer implantação que não seja de um único usuário confiável.

Tratamento de erros

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: ← Avaliação | Próximo: Autorização de ferramentas →

endpoints de A2A

RotaAutenticação
GET /.well-known/agent.jsonpública — os pares buscam o cartão antes de possuírem uma credencial
POST /a2aobrigatória, quando um RequestContextExtractor está configurado
POST /a2a/streamobrigatório, quando um RequestContextExtractor está configurado

As rotas JSON-RPC executam o trabalho do agente e das ferramentas, portanto usam a mesma camada que os roteadores de sessão, artefato e depuração. Sem um extrator configurado, não há credencial a exigir e as rotas permanecem abertas; assim, adicionar o controle não interrompe uma implantação existente.

Importante: essas rotas anteriormente eram mescladas na raiz do roteador, fora da camada aplicada a /api. Uma implantação que autenticava todas as outras superfícies de mutação ainda permitia que qualquer cliente capaz de alcançar a porta controlasse o agente e incorresse em seu custo. Isso se aplicava tanto a create_app_with_a2a quanto a ServerBuilder::build.

A2aServer::builder() vincula 127.0.0.1:8080 por padrão. Chame bind_addr para expô-lo, e configure um extrator antes de fazer isso. O esqueleto a2a-server gerado segue a mesma regra e lê BIND_HOST para optar por um vínculo mais amplo.