التحكم في الوصول
التحكم في الوصول على مستوى المؤسسات لوكلاء الذكاء الاصطناعي باستخدام 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.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
مقدمو الخدمة المدعومون
| المزوّد | الباني | الجهة المُصدِرة |
|---|---|---|
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 | 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),
);
| Rule | السلوك |
|---|---|
| الأداة لديها تفويض يغطي الاسم | مسموح |
| الأداة لديها تفويض لا يغطي الاسم | مرفوض؛ لا يتم استدعاء المزوّد أبدًا |
| الأداة لا تملك منحة | مرفوض |
| الطلب لا يحمل هوية أداة | مرفوض إلا إذا فتحه grant_untooled |
كل شيء مرفوض حتى يُمنح، ويؤدي الرفض إلى إرجاع خطأ Unauthorized. ولا يتم البحث عن الاسم المرفوض إطلاقًا، لذلك لا يظهر في سجلات وصول جهة المورِّد كأنه محاولة قراءة.
الهوية ليست شيئًا يصرّح به tool. يطبع LlmAgent اسم الأداة المرسلة على الطلب، إلى جانب app وuser وsession وinvocation، لذلك لا يمكن لأداة أن تنتحل هوية أداة أخرى. ويمكن للأداة إضافة purpose فقط:
// inside a tool
let key = ctx.get_secret_for_purpose("weather-api-key", "call the forecast endpoint").await?;
ملاحظة: agent مُستدعى كأداة يعبر حدود
ToolContext، والتي لا تحمل أي هوية خاصة بها، لذا فإن عمليات الوصول التي تُجرى داخل ذلك agent تعرض هوية agent الخارجي بدلًا من هوية الأداة الداخلية. امنح الصلاحيات وفقًا لذلك.
تدقيق الوصول
يتلقى SecretAuditSink واحدًا من SecretAccessDecision لكل قرار، يتضمن
النتيجة، واسم السر، وtool، وuser، وinvocation، والسبب — ولا يتضمن أبدًا قيمة سرية. كما تُسجَّل عمليات السماح عند 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/*، المنتجات، التصحيح | 401 بدون رمز صالح |
/api/ui/* — الجسر، الإشعارات، الموارد | 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 للانتقال إلى ربط أوسع.