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 escopo, registro de auditoria e suporte a SSO para agentes ADK. Ele permite controle seguro e refinado 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 de Negação

Quando uma função tem regras de permitir e negar, negar sempre vence:

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 múltiplas funções получают 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 em vez de 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 (via JWT)
  • Autorização (RBAC): Determina O QUÊ 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.0.0"

# For SSO/OAuth support
adk-auth = { version = "2.0.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

Envolve 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 em lote várias ferramentas:

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

ScopeGuard

Use escopos para autorização no nível da requisição que vem de claims de 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 "este usuário pode acessar a ferramenta de forma alguma?" Escopos respondem "esta requisição específica está autorizada agora?"

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 Suportados

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 tenant para o audience configurado, a menos que você o restrinja explicitamente com with_allowed_tenants(...).

TokenClaims

Claims 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 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() só usa o claim de email 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 Personalizado de Auditoria

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

Boas Práticas de Segurança

PráticaDescrição
Negar por padrãoConceda apenas as permissões explicitamente necessárias
Negações explícitasAdicione regras de negação para operações perigosas
Audite tudoAtive o registro para conformidade
Valide no lado do servidorSempre valide JWTs no servidor
Use HTTPSOs endpoints JWKS exigem conexões seguras
Gire as chavesO cache JWKS é atualizado automaticamente a cada hora
Limitar a vida útil do tokenUse tokens de acesso de curta duração
Restringir tenants do AzurePara apps Azure multitenant, configure with_allowed_tenants(...)
Verificar e-mail antes do mapeamento de identidadeuser_id_from_email() agora recorre a sub quando o e-mail não está verificado
Planejar a revogaçãoA revogação de token não é incorporada; aplique-a em um validador personalizado se você precisar de corte imediato
Cache de pesquisas de escopo carasSe suas ScopeResolver chamarem sistemas externos, armazene o resultado em cache por request/session

Ponte de Autenticação

Habilite auth-bridge quando quiser que adk-auth forneça um extrator de requisição 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 claims JWT scope / scp para RequestContext.scopes.

Provedores de Segredos

Ferramentas acessam segredos em tempo de execução por meio de ToolContext::get_secret e InvocationContext::get_secret. Por trás disso está adk_auth::secrets::SecretProvider, com implementações em nuvem por trás de flags de recurso:

ProvedorFuncionalidade
AWS Secrets Manageraws-secrets
Azure Key Vault (cofre de chaves)azure-keyvault
Gerenciador de Segredos do GCPgcp-secrets

Anexe um serviço a uma execução encapsulando-o como um 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 possui 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 de a 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 cobre o nomePermitido
A ferramenta tem uma concessão que não cobre o nomeNegado; o provedor nunca é chamado
A ferramenta não possui concessãoNegado
A requisição não contém identidade de ferramentaNegado, a menos que grant_untooled o abra

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

A identidade não é algo que uma tool afirma. LlmAgent carimba o nome da tool despachada na request, junto com o app, o usuário, a session e a invocation, de modo que uma tool não pode apresentar a identidade de outra tool. Uma tool pode adicionar apenas um purpose:

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

Note: um agent invocado como uma tool atravessa um ToolContext, que não carrega identidade própria, então os acessos feitos dentro desse agent apresentam a identidade do agent externo, em vez da tool interna. Conceda de acordo.

Auditing Access

SecretAuditSink recebe um SecretAccessDecision por decisão, carregando o resultado, o nome secreto, a tool, o usuário, a invocation e o motivo — e nunca um valor secreto. Allows também são registrados em info e denials em warn.

Caching

CachedSecretProvider serve um value para o seu TTL, e então faz refetch. É limitado e revogável:

ControleComportamento
with_max_entries(n)No máximo n nomes em cache; o menos recentemente usado é descartado quando estiver cheio. O padrão é 128; 0 desativa o cache
invalidate(name)Remove um segredo imediatamente — use isso quando um segredo for rotacionado para que o valor antigo não seja servido pelo restante de sua TTL
invalidate_all()Descarta tudo
purge_expired()Descarta entradas expiradas sem esperar que sejam lidas novamente

Um limite importa quando nomes secretos são derivados da entrada: sem ele, o cache pode crescer durante toda a vida útil do processo.

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

Uma 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 são invalidadas, o que reduz a permanência para aproximadamente TTL. Isso é uma redução, não uma eliminação — uma String pode já ter sido realocada, copiada pelo alocador, gravada em disco ou capturada em um core dump. A saída de depuração para o cache é redigida para que uma impressão de diagnóstico não possa vazar um valor.

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

O que o extrator protege

Configurar um extrator ativa a autenticação para toda rota não pública:

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 requisição
/api/run*401 sem um token válido; o usuário autenticado substitui o usuário fornecido
/healthPúblico

O estado da ponte da UI é indexado por (app_name, user_id, session_id) obtido do corpo da requisição, então o usuário autenticado é substituído pelo valor do corpo em vez de ser confiável. Um recurso de UI registrado registra 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 404, de modo que a existência do URI não é revelada.

Sem nenhum extractor configurado, não há identidade autenticada para vincular, então as rotas permanecem abertas e os recursos ficam globalmente visíveis. A autenticação é opcional — configure um extractor para qualquer implantação que não seja 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 ferramenta →

pontos de extremidade A2A

RotaAutenticação
GET /.well-known/agent.jsonpúblico — os pares obtêm o cartão antes de terem uma credencial
POST /a2aobrigatório, quando um RequestContextExtractor está configurado
POST /a2a/streamobrigatório, quando um RequestContextExtractor é configurado

As rotas JSON-RPC executam o trabalho do agente e da ferramenta, então elas carregam a mesma camada que os roteadores de sessão, artefato e debug. Sem um extractor configurado, não há credencial a ser exigida e as rotas permanecem abertas, então adicionar a gate não quebra uma implantação existente.

Importante: essas rotas eram anteriormente mescladas na raiz do roteador, fora da camada aplicada a /api. Uma implantação que autenticava toda outra superfície de mutação ainda permitia que qualquer cliente que conseguisse alcançar a porta conduzisse o agente e arcasse com seu custo. Isso se aplicava tanto a create_app_with_a2a quanto a ServerBuilder::build.

A2aServer::builder() faz bind em 127.0.0.1:8080 por padrão. Chame bind_addr para expô-lo e configure um extractor antes disso. O scaffold gerado de a2a-server segue a mesma regra e lê BIND_HOST para optar por um bind mais amplo.

Controle de Acesso - Documentação ADK-Rust | ADK-Rust