访问控制

使用 adk-auth 为 AI agent 提供企业级访问控制。

概述

adk-auth 为 ADK agent 提供基于角色的访问控制(RBAC)、基于范围的授权、审计日志记录和 SSO 支持。它能够安全、细粒度地控制哪些用户可以访问哪些工具。

架构

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

设计原则

1. 拒绝优先

当一个角色同时具有允许规则和拒绝规则时,拒绝始终优先:

let role = Role::new("limited")
    .allow(Permission::AllTools)      // Allow everything...
    .deny(Permission::Tool("admin")); // ...except admin

// Result: Can access any tool EXCEPT "admin"

2. 多角色合并

拥有多个角色的用户获得权限的并集,但任何角色中的拒绝规则仍然适用:

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. 显式优于隐式

权限必须显式授予——默认不授予任何访问权限:

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

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

4. 身份验证与授权分离

  • 身份验证(SSO):验证用户是谁(通过 JWT)
  • 授权(RBAC):确定用户可以访问什么
// 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?;

安装

[dependencies]
adk-auth = "2.1.0"

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

核心组件

权限

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

角色

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

使用自动权限检查封装工具:

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

批量保护多个工具:

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

ScopeGuard

使用范围执行请求级授权,该授权来自 JWT 声明或会话状态:

use adk_auth::{ContextScopeResolver, ScopeGuard};

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

组合使用 RBAC + 范围

RBAC 回答“该用户是否完全有权访问此工具?”范围回答“此特定请求当前是否已获授权?”

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));

SSO 集成

支持的提供商

提供商构造函数发行方
GoogleGoogleProvider::new(client_id)accounts.google.com
Azure ADAzureADProvider::new(tenant, client) 或 AzureADProvider::multi_tenant(client).with_allowed_tenants(["tenant-id"])login.microsoftonline.com
OktaOktaProvider::new(domain, client){domain}/oauth2/default
Auth0Auth0Provider::new(domain, audience){domain}/
通用OidcProvider::from_discovery(issuer, client)任意 OIDC 提供商

AzureADProvider::multi_tenant() 接受面向所配置受众的任何租户,除非你使用 with_allowed_tenants(...) 明确限制租户。

TokenClaims

从已验证的 JWTs 中提取的声明:

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

将 IdP 组映射到 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() 仅在 email_verified == true 时使用电子邮件声明;否则回退到 sub。

SsoAccessControl

在一次调用中将 SSO 验证与 RBAC 结合:

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?;

审计日志记录

FileAuditSink

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

输出格式(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"}

自定义审计接收器

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(())
    }
}

示例

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

安全最佳实践

实践描述
默认拒绝仅授予明确需要的权限
显式拒绝为危险操作添加拒绝规则
审计所有内容启用日志记录以满足合规要求
在服务器端验证始终在服务器上验证 JWTs
使用 HTTPSJWKS 端点要求使用安全连接
轮换密钥JWKS 缓存每小时自动刷新
限制令牌生命周期使用短生命周期的访问令牌
限制 Azure 租户对于多租户 Azure 应用,配置 with_allowed_tenants(...)
在身份映射前验证电子邮件现在,当电子邮件未经验证时,user_id_from_email() 会回退到 sub
为撤销做好规划令牌撤销并非内置功能;如果需要立即终止访问,请在自定义验证器中强制执行
缓存开销较大的作用域查找如果您的 ScopeResolver 调用外部系统,请按请求/会话缓存结果

身份验证桥接

当你希望 adk-auth 为 adk-server 提供可复用的基于 JWT 的请求提取器时,请启用 auth-bridge:

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()?;

该提取器会验证 Bearer 令牌,使用 ClaimsMapper 映射 user_id,并将 JWT scope / scp 声明转发到 RequestContext.scopes。

密钥提供程序

工具通过 ToolContext::get_secret 和 InvocationContext::get_secret 访问运行时密钥。这些接口背后是 adk_auth::secrets::SecretProvider, 云实现则位于功能标志之后:

提供商功能
AWS Secrets Manageraws-secrets
Azure Key Vaultazure-keyvault
GCP 密钥管理器gcp-secrets

将其包装为 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));

按工具授权

默认情况下,持有上下文的工具可以命名任何密钥,而提供方只能看到该名称——无法区分天气工具是在请求自己的 API 密钥,还是同一工具在请求支付凭据。AuthorizingSecretService 会在联系提供方之前,针对每个工具作出决定:

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),
);
规则行为
工具具有涵盖该名称的授权允许
工具具有不涵盖该名称的授权拒绝;提供方永远不会被调用
工具没有授权拒绝
请求不携带工具标识拒绝,除非 grant_untooled 打开它

除非获得授权,否则一律拒绝;拒绝会返回 Unauthorized 错误。被拒绝的名称永远不会被查找,因此不会作为尝试读取出现在提供方一侧的访问日志中。

身份并不是由工具声明的。LlmAgent 会将被调度工具的名称标记到请求上,并附带应用、用户、会话和调用信息,因此工具无法冒充其他工具的身份。工具只能添加一个用途:

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

注意: 作为工具调用的代理会跨越一个 ToolContext,该调用不携带自身身份,因此在该代理内部进行的访问所呈现的是外层代理的身份,而不是内部工具的身份。请相应地授予权限。

审计访问

SecretAuditSink 会针对每个决策接收一个 SecretAccessDecision,其中包含结果、机密名称、工具、用户、调用和原因——绝不会包含机密值。允许操作也会记录在 info,拒绝操作则记录在 warn。

缓存

CachedSecretProvider 会在其 TTL 期间提供一个值,之后重新获取。它具有大小限制,并且可以撤销:

控制项行为
with_max_entries(n)最多缓存 n 个名称;缓存已满时会丢弃最近最少使用的名称。默认为 128;0 可禁用缓存
invalidate(name)立即丢弃一个密钥——密钥轮换时使用此项,这样旧值在其 TTL 剩余时间内不会被提供
invalidate_all()丢弃所有内容
purge_expired()丢弃已过期条目,无需等待再次读取

当机密名称源自输入时,边界就很重要:没有边界,缓存可能在进程的整个生命周期内不断增长。

缓存保证什么以及不保证什么

TTL控制缓存返回的内容,而不控制某个值在进程内存中保留多久。条目在过期、被逐出或失效时会被清零,这会将驻留时间缩短至大约 TTL。这是一种减少,而不是擦除——String可能已经被重新分配、被分配器复制、换出到磁盘,或被捕获在核心转储中。缓存的调试输出会进行删减,因此诊断打印不会泄露值。

**重要:**单独的 SecretProvider本身不应用任何策略——任何持有上下文的工具都可以请求底层凭据能够读取的任意名称。将其包装在 AuthorizingSecretService 中即可获得每个工具的边界;同时仍要限定云凭据本身:每个部署使用一个 IAM 身份,并且只能访问该部署所需的机密。 **重要:**提供程序接口只接受机密名称。在 ADK 层没有每个工具的授权、命名空间或访问审计,因此任何持有上下文的工具都可以请求底层凭据能够读取的任意名称。请限定云凭据本身——每个部署使用一个 IAM 身份,并且只能访问该部署所需的机密——并将提供程序端的审计日志作为访问记录。

提取器保护什么

配置提取器会为每个非公开路由启用身份验证:

路由配置提取器时的行为
/api/sessions/*, /api/apps/*, artifacts, debug没有有效令牌时返回 401
/api/ui/* — bridge, notifications, resources没有有效令牌时返回 401;经过身份验证的用户会替换请求正文中指定的任何用户
/api/run*没有有效令牌时返回 401;经过身份验证的用户会覆盖所提供的用户
/health公开

UI bridge 状态由从请求正文中获取的 (app_name, user_id, session_id) 作为键,因此经过身份验证的用户会替代正文中的值,而不是信任该值。已注册的 UI 资源会记录注册该资源的用户;只有该用户可以读取或替换它,而读取其他用户的资源会返回 404,因此不会泄露 URI 的存在。

未配置提取器时,没有可绑定的经过身份验证的身份,因此路由保持开放,资源对所有用户全局可见。身份验证是可选启用的——对于并非由单一受信任用户使用的任何部署,都应配置提取器。

错误处理

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 */ }
}

上一页:← 评估 | 下一页:工具授权 →

A2A 端点

路由身份验证
GET /.well-known/agent.json公开 — 对等方在持有凭据之前获取卡片
POST /a2a必需,配置 RequestContextExtractor 时
POST /a2a/stream配置了 RequestContextExtractor 时必需

JSON-RPC 路由执行代理和工具工作,因此它们与会话、构件和调试路由处于同一层。未配置提取器时,不存在需要验证的凭据,路由会保持开放,因此添加此门禁不会破坏现有部署。

重要: 这些路由之前合并在路由器根部,位于应用于 /api 的层之外。即使部署已对其他所有变更操作入口进行身份验证,任何能够访问该端口的客户端仍可驱动代理并产生相应成本。这同时适用于 create_app_with_a2a 和 ServerBuilder::build。

A2aServer::builder() 默认绑定 127.0.0.1:8080。调用 bind_addr 以公开它,并在此之前配置提取器。生成的 a2a-server 脚手架遵循相同规则,并读取 BIND_HOST 以选择绑定更广泛的地址。