التحكم في الوصول
تحكم في الوصول بمستوى المؤسسات للوكلاء الذكيين باستخدام 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
موفرو الخدمة المدعومون
| الموفر | المُنشئ | جهة الإصدار |
|---|---|---|
GoogleProvider::new(client_id) | accounts.google.com | |
| Azure AD | AzureADProvider::new(tenant, client) أو 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}/ |
| عام | 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 Manager | aws-secrets |
| Azure Key Vault | azure-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 | عام |
تُحدَّد حالة جسر واجهة المستخدم بواسطة (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 للسماح بربط أوسع.