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
| Provedor | Construtor | Emissor |
|---|---|---|
GoogleProvider::new(client_id) | accounts.google.com | |
| Azure AD | AzureADProvider::new(tenant, client) ou 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) | 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ática | Descrição |
|---|---|
| Negar por padrão | Conceda apenas as permissões explicitamente necessárias |
| Negações explícitas | Adicione regras de negação para operações perigosas |
| Audite tudo | Ative o registro para conformidade |
| Valide no lado do servidor | Sempre valide JWTs no servidor |
| Use HTTPS | Os endpoints JWKS exigem conexões seguras |
| Gire as chaves | O cache JWKS é atualizado automaticamente a cada hora |
| Limitar a vida útil do token | Use tokens de acesso de curta duração |
| Restringir tenants do Azure | Para apps Azure multitenant, configure with_allowed_tenants(...) |
| Verificar e-mail antes do mapeamento de identidade | user_id_from_email() agora recorre a sub quando o e-mail não está verificado |
| Planejar a revogação | A revogação de token não é incorporada; aplique-a em um validador personalizado se você precisar de corte imediato |
| Cache de pesquisas de escopo caras | Se 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:
| Provedor | Funcionalidade |
|---|---|
| AWS Secrets Manager | aws-secrets |
| Azure Key Vault (cofre de chaves) | azure-keyvault |
| Gerenciador de Segredos do GCP | gcp-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),
);
| Regra | Comportamento |
|---|---|
| A ferramenta tem uma concessão que cobre o nome | Permitido |
| A ferramenta tem uma concessão que não cobre o nome | Negado; o provedor nunca é chamado |
| A ferramenta não possui concessão | Negado |
| A requisição não contém identidade de ferramenta | Negado, 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:
| Controle | Comportamento |
|---|---|
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
SecretProviderpuro 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 emAuthorizingSecretServicepara 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:
| Rotas | Comportamento com um extrator configurado |
|---|---|
/api/sessions/*, /api/apps/*, artifacts, debug | 401 sem um token válido |
/api/ui/* — bridge, notifications, resources | 401 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 |
/health | Pú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
| Rota | Autenticação |
|---|---|
GET /.well-known/agent.json | público — os pares obtêm o cartão antes de terem uma credencial |
POST /a2a | obrigatório, quando um RequestContextExtractor está configurado |
POST /a2a/stream | obrigató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 acreate_app_with_a2aquanto aServerBuilder::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.