التحكم في الوصول

تحكم في الوصول بمستوى المؤسسات للوكلاء الذكيين باستخدام adk-auth.

نظرة عامة

يوفر 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.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 على الخادم
استخدم HTTPSتتطلب نقاط نهاية JWKS اتصالات آمنة
تدوير المفاتيحتُحدّث ذاكرة التخزين المؤقت لـ JWKS تلقائيًا كل ساعة
حدّد عمر الرمزاستخدم رموز وصول قصيرة الأجل
قيّد مستأجري Azureبالنسبة إلى تطبيقات Azure متعددة المستأجرين، اضبط with_allowed_tenants(...)
تحقّق من البريد الإلكتروني قبل تعيين الهويةيعود user_id_from_email() الآن إلى sub عندما يكون البريد الإلكتروني غير موثَّق
خطّط للإلغاءإلغاء الرمز غير مضمَّن؛ افرضه في أداة تحقق مخصّصة إذا كنت بحاجة إلى إيقاف فوري
خزّن مؤقتًا عمليات البحث المكلفة عن النطاقاتإذا كانت ScopeResolver تستدعي أنظمة خارجية، فخزّن النتيجة مؤقتًا لكل طلب/جلسة

جسر المصادقة

فعّل auth-bridge عندما تريد أن يوفر adk-auth مستخرج طلبات قابلًا لإعادة الاستخدام يستند إلى JWT من أجل 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()?;

يتحقق المستخرج من رمز Bearer، ويعيّن user_id باستخدام ClaimsMapper، ويمرّر مطالبات JWT scope / scp إلى RequestContext.scopes.

موفرو الأسرار

تصل الأدوات إلى أسرار وقت التشغيل عبر ToolContext::get_secret و InvocationContext::get_secret. ويقع خلفهما adk_auth::secrets::SecretProvider، مع تطبيقات سحابية خلف أعلام الميزات:

المزوّدالميزة
AWS Secrets Manageraws-secrets
Azure Key Vaultazure-keyvault
مدير الأسرار في GCPgcp-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، debug401 بدون رمز صالح
/api/ui/* — bridge، notifications، resources401 بدون رمز صالح؛ يحلّ المستخدم المُصادَق عليه محل أي مستخدم مُسمّى في نص الطلب
/api/run*401 بدون رمز صالح؛ يتجاوز المستخدم المصادق عليه المستخدم المُقدَّم
/healthعام

تُحدَّد حالة جسر واجهة المستخدم بواسطة (app_name, user_id, session_id) المأخوذ من نص الطلب، ولذلك يُستبدل المستخدم المُصادَق عليه بقيمة النص بدلاً من الوثوق بها. يسجّل مورد واجهة مستخدم مسجّل المستخدم الذي سجّله؛ ولا يمكن قراءته أو استبداله إلا من قِبل ذلك المستخدم، كما أن قراءة مورد يخص مستخدمًا آخر تُرجع 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 للسماح بربط أوسع.

التحكم في الوصول - وثائق ADK-Rust | ADK-Rust