アクセス制御

adk-auth を使用した、AI エージェント向けのエンタープライズグレードのアクセス制御。

概要

adk-auth は、ロールベースアクセス制御(RBAC)、スコープベースの認可、監査ログ、および SSO サポートを ADK エージェントに提供します。これにより、どのユーザーがどのツールにアクセスできるかを、きめ細かく安全に制御できます。

アーキテクチャ

┌─────────────────────────────────────────────────────────────────┐
│                        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.0.0"

# For SSO/OAuth support
adk-auth = { version = "2.0.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(...) で明示的に制限しない限り、構成された audience に対して任意の tenant を受け入れます。

TokenClaims

検証済みの JWTs から抽出された claims:

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 groups を adk-auth roles にマッピングします:

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 の場合にのみ email claim を使用します。それ以外の場合は sub にフォールバックします。

SsoAccessControl

SSO validation と RBAC を 1 回の呼び出しで組み合わせます:

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 を検証する
HTTPS を使用するJWKS エンドポイントには安全な接続が必要
鍵をローテーションするJWKS キャッシュは 1 時間ごとに自動更新される
トークンの有効期間を短くする短命なアクセス トークンを使用する
Azure テナントを制限するマルチテナント Azure アプリでは、with_allowed_tenants(...) を構成する
ID マッピングの前にメールを検証するメールが未検証の場合、user_id_from_email() は現在 sub にフォールバックする
失効を考慮するトークンの失効は組み込みではない。即時に遮断したい場合は、カスタム バリデーターで強制する
高コストなスコープ検索をキャッシュするもし ScopeResolver が外部システムを呼び出す場合は、結果をリクエストごと/セッションごとにキャッシュします

認証ブリッジ

auth-bridge を有効にすると、adk-authadk-server 向けに再利用可能な JWT ベースのリクエスト抽出器を提供します。

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 トークンを検証し、ClaimsMapperuser_id をマッピングして、JWT の scope / scp クレームを RequestContext.scopes に渡します。

シークレットプロバイダー

ツールは ToolContext::get_secretInvocationContext::get_secret を介して実行時のシークレットにアクセスします。その背後には adk_auth::secrets::SecretProvider があり、クラウド実装は機能フラグで有効になります。

プロバイダー機能
AWS Secrets Manageraws-secrets
Azure Key Vault(キー保管庫)azure-keyvault
GCP Secret Managergcp-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),
);
ルール動作
Tool に名前を含む grant がある許可
Tool に名前を含まない grant がある拒否; provider は決して呼び出されない
ツールに権限がない拒否
リクエストにツールの識別情報がないgrant_untooled がそれを開く場合を除き拒否

すべては許可されるまで拒否され、拒否は Unauthorized エラーを返します。拒否された名前は決して検索されないため、試行された読み取りとしてプロバイダー側のアクセスログに表示されることはありません。

アイデンティティは、ツールが主張するものではありません。LlmAgent は、アプリ、ユーザー、セッション、呼び出しとともに、実行されたツールの名前をリクエストに刻み込みます。そのため、ツールは別のツールのアイデンティティを提示できません。ツールが追加できるのは purpose だけです:

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

Note: ツールとして呼び出されたエージェントは、固有のアイデンティティを持たない ToolContext をまたぐため、その内部で行われたアクセスは、内側のツールではなく外側のエージェントのアイデンティティを示します。許可はそれに応じて与えてください。

アクセスの監査

SecretAuditSink は、各決定ごとに 1 つの SecretAccessDecision を受け取り、結果、秘密名、ツール、ユーザー、呼び出し、理由を含みます。秘密値は決して含まれません。許可は info でも記録され、拒否は warn で記録されます。

キャッシング

CachedSecretProviderはそのTTLに値を提供し、その後再取得します。これは制限付きで、取り消し可能です:

制御動作
with_max_entries(n)最大で n 個の名前をキャッシュします。満杯になると、最も最近使用されていないものが破棄されます。デフォルトは 128 です。0 はキャッシュを無効にします
invalidate(name)1 つのシークレットを即座に破棄します。シークレットがローテーションされたときにこれを使用すると、古い値がその TTL の残りの期間にわたって返されません
invalidate_all()すべてを削除
purge_expired()再度読み取られるのを待たずに期限切れのエントリを削除する

入力から秘密名が導出される場合は、上限が重要です。上限がないと、キャッシュはプロセスの存続期間中ずっと増え続ける可能性があります。

キャッシュが保証することと、しないこと

TTL は、キャッシュが何を返すかを制御するものであり、値がプロセスのメモリ内にどれだけ長く残るかを制御するものではありません。エントリは期限切れ、追い出し、または無効化されたときにゼロ化されるため、滞留期間はおおむね TTL まで短縮されます。これは削減であって消去ではありません。String はすでに再割り当てされているかもしれず、アロケータによってコピーされているか、ディスクにスワップされているか、コアダンプに捕捉されている可能性があります。キャッシュのデバッグ出力はマスキングされるため、診断用の出力で値が漏れることはありません。

重要: 単独の SecretProvider にはそれ自体のポリシーは適用されません。コンテキストを持つ任意のツールは、バックエンドの認証情報が読み取れる任意の名前を要求できます。AuthorizingSecretService で包むことでツールごとの境界を設けられます。さらに、クラウド認証情報自体もスコープを限定してください。つまり、各デプロイメントごとに 1 つの IAM ID を用意し、そのデプロイメントに必要なシークレットのみにアクセスさせます。 重要: プロバイダのインターフェースが受け取るのはシークレットの 名前 だけです。ADK レイヤーには、ツールごとの許可、名前空間、アクセス監査はありません。そのため、コンテキストを持つ任意のツールは、バックエンドの認証情報が読み取れる任意の名前を要求できます。クラウド認証情報自体のスコープを限定し、つまり各デプロイメントごとに 1 つの IAM ID を用意して、そのデプロイメントに必要なシークレットのみにアクセスさせ、プロバイダ側の監査ログをアクセス記録として扱ってください。

エクストラクタが保護するもの

エクストラクタを設定すると、すべての非公開ルートで認証が有効になります:

ルートエクストラクタが設定されている場合の動作
/api/sessions/*, /api/apps/*, artifacts, debug有効なトークンがない場合は 401
/api/ui/* — bridge, notifications, resources有効なトークンがない場合は 401; 認証済みユーザーがリクエスト本文で指定されたユーザーを置き換える
/api/run*有効なトークンがない場合の 401。認証済みユーザーは指定されたユーザーより優先される
/health公開

UI bridge state は、リクエスト本文から取得された (app_name, user_id, session_id) をキーにしており、そのため認証済みユーザーは本文の値ではなく置き換えられて、信頼されません。登録された UI リソースは、それを登録したユーザーを記録します。読み取りや置き換えができるのはそのユーザーだけで、他人のリソースを読み取ろうとすると 404 が返されるため、URI の存在は開示されません。

エクストラクタ が設定されていない場合は、関連付ける認証済みの ID がないため、ルートは開放されたままで、リソースはグローバルに可視のままになります。認証はオプトインです。単一の信頼できるユーザーではない任意のデプロイメントでは、エクストラクタを設定してください。

エラーハンドリング

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

前へ: ← Evaluation | 次へ: Tool Authorization →

A2A エンドポイント

ルート認証
GET /.well-known/agent.jsonpublic — ピアは資格情報を保持する前にカードを取得します
POST /a2aRequestContextExtractor が設定されている場合は必須
POST /a2a/streamRequestContextExtractor が設定されている場合は必須

JSON-RPC ルートはエージェントとツールの処理を実行するため、セッション、 アーティファクト、デバッグの各ルーターと同じレイヤーを持ちます。extractor が設定されていない場合、要求する認証情報がないため、 ルートは開いたままになり、ゲートを追加しても既存のデプロイメントは壊れません。

重要: これらのルートは以前、ルーターのルート直下で、/api に適用されたレイヤーの外側にマージされていました。ほかのすべての変更系の面を認証していたデプロイメントでも、ポートに到達できるクライアントなら誰でもエージェントを操作し、そのコストを発生させることができました。これは create_app_with_a2aServerBuilder::build の両方に当てはまります。

A2aServer::builder() はデフォルトで 127.0.0.1:8080 にバインドします。公開するには bind_addr を呼び出し、その前に extractor を設定してください。生成された a2a-server のスキャフォールドも同じルールに従い、より広いバインドを選ぶために BIND_HOST を読み取ります。

アクセス制御 - ADK-Rust ドキュメント | ADK-Rust