← همه یادداشت‌هاEnglish ↗
معماری مجوزدهی۸ دقیقه مطالعه

وقتی RBAC کافی نیست: نقش، واحد و زمینه بدون انفجار نقش‌ها

چگونه قابلیت، محدوده سازمانی و شرایط زمان اجرا را در مدل مجوزدهی جدا کنیم؛ همراه با مقایسه تعاملی مدل‌ها، شبیه‌ساز رشد نقش‌ها و بررسی مسیر تصمیم.

در این مقاله

مدل RBAC ساده و منظم است؛ تا وقتی که یک قابلیت در یک محدوده سازمانی معتبر باشد، در محدوده‌ای دیگر نباشد، جایی موقت باشد و همچنان به وضعیت منبع وابسته بماند. مسئله طراحی این نیست که «چند نقش لازم دارم؟» بلکه این است که «کدام ابعاد واقعاً متعلق به نقش هستند؟»

مطالعه موردی با داده‌های ساختگی: تمام نام نقش‌ها، برچسب واحدها، شناسه‌ها، مقادیر توکن، وضعیت‌های گردش کار و مثال‌های این مقاله ساختگی هستند. هیچ نام سازمان یا کارمند، نشانی داخلی، شناسه محیط عملیاتی، نام اختصاصی نقطه پایانی یا داده عملیاتی منتشرنشده‌ای در آن وجود ندارد.

جایی که RBAC تخت به مرز خود رسید

من با RBAC تخت و متعارف شروع کردم: User → Role → Permission. نقش‌ها نمایانگر قابلیت‌های قابل استفاده مجدد بودند، مجوزها مستقیماً به کاربران اختصاص نمی‌یافتند و یک منبع هویتی معتبر می‌توانست طبقه‌بندی شغلی فرد را به یک یا چند نقش پیش‌فرض نگاشت کند.

وقتی مرزهای سازمانی وارد تصمیم شدند، این مدل دیگر کافی نبود. کاربر صرفاً یک سرپرست نبود؛ نقش او در محدوده‌ای مشخص اعتبار داشت. همان فرد ممکن بود در جایی دیگر نقش متفاوتی داشته باشد. حتی نقش درست در واحد درست هم می‌توانست به دلیل وضعیت گردش کار، مالکیت یا قواعد تفکیک وظایف به رد درخواست منجر شود.

اگر این ابعاد را در نام نقش‌هایی هرچه اختصاصی‌تر رمزگذاری کنیم، فهرست نقش‌ها به بازنمایی نمودار سازمانی تبدیل می‌شود، نه مدل مجوزدهی.

زمینه را از هویت نقش جدا کنید

نقش‌های ترکیبی را با تخصیص‌های محدوده‌دار مقایسه کنید. مدل مجوز همچنان RBAC است؛ اختیار سازمانی به تخصیص منتقل می‌شود.

شخص
UNIT-A_SUPERVISOR_MODULE-X
UNIT-B_SUPERVISOR_MODULE-X
UNIT-C_SUPERVISOR_MODULE-X
UNIT-D_SUPERVISOR_MODULE-X

تغییر جایگاه سازمانی، شناسه نقش را هم تغییر می‌دهد.

تغییر معماری، تعریف سه مفهوم جداگانه بود:

Role    = WHAT operations may be performed?
Scope   = WHERE is that role assignment valid?
Context = WHAT runtime facts must also be true?

نقش مشخص می‌کند چه عملیاتی مجاز است، محدوده مشخص می‌کند کجا اعتبار دارد و زمینه مشخص می‌کند کدام شرایط زمان اجرا باید برقرار باشند.

بنابراین رابطه کاربر و نقش به یک تخصیص با محدوده و چرخه حیات تبدیل شد:

RoleAssignment(
    principalId,
    roleId,
    scopeType,
    scopeId,
    source,
    validFrom,
    validTo,
    status
)

طبقه‌بندی شغلی همچنان برای ایجاد تخصیص پیش‌فرض مفید است؛ اما شرط مجوزدهی در سرویس‌های کسب‌وکار نیست. به این ترتیب، رده‌بندی منابع انسانی، سیاست مجوزدهی و منطق دامنه در یکدیگر ادغام نمی‌شوند.

تابع مجوزدهی مورد انتظار از مدل

کمیت‌گذار کلیدی این است: «حداقل یک تخصیص فعال وجود داشته باشد» که هم عملیات را مجاز بداند و هم محدوده منبع را پوشش دهد.

authorize(p, op, r, ctx) =
 validToken(ctx.token)
 ∧ ∃ a ∈ activeAssignments(p, ctx.time):
      op ∈ permissions(a.role)
      ∧ covers(a.scope, scopeOf(r))
 ∧ runtimeRules(p, op, r, ctx)

نتیجه، یک تصمیم صریح است: اجازه یا رد.

قواعدی که پیاده‌سازی نباید نقض کند

این قواعد از توصیه‌های کلی مفیدترند؛ زیرا می‌توان آن‌ها را بازبینی، آزمون و اجرا کرد.

  • I1: مجوزها هرگز مستقیماً به اشخاص اختصاص نمی‌یابند.
  • I2: نقش و محدوده در همان تخصیص به یکدیگر متصل می‌مانند.
  • I3: شناسه واحد ارسالی کاربر، مدرک مجوزدهی نیست.
  • I4: تخصیص منقضی یا لغوشده در تصمیم مشارکت نمی‌کند.
  • I5: ورودی ناشناخته در فرایند ایجاد تخصیص، هیچ اختیار عملیاتی تولید نمی‌کند.
  • I6: قواعد گردش کار و ایجادکننده/تأییدکننده روی منبع واقعی ارزیابی می‌شوند.

چرا محدوده نباید به هویت نقش تبدیل شود

این محاسبه یک کران بالای ترکیبی است؛ نه ادعای وجود تمام ترکیب‌ها. هدف، نشان دادن سرعت رشد فضای نام نقش‌هاست.

ابعاد مدل را تغییر دهید
شناسه‌های ترکیبی ممکن۸۰۰R × S × M
با تخصیص محدوده‌دار۴ نقش قابل استفاده مجدد

مرز توکن محل بروز خطای دوم است. اگر نقش‌ها و محدوده‌ها در آرایه‌های مستقل منتشر شوند، کد مصرف‌کننده ممکن است ناخواسته آن‌ها را یک حاصل‌ضرب دکارتی در نظر بگیرد.

پیوند نقش و محدوده را حفظ کنید

ساختار اول مبهم است. ساختار دوم دقیقاً حفظ می‌کند که هر نقش در کدام محدوده معتبر است.

ساختار مبهم ادعاها

{
  "roles": ["SUPERVISOR", "OPERATOR"],
  "scopes": ["UNIT-A", "UNIT-B"]
}

ساختار تخصیص با پیوند صریح

{
  "roleAssignments": [
    {
      "role": "SUPERVISOR",
      "scope": { "type": "ORG_UNIT", "id": "UNIT-A" }
    },
    {
      "role": "OPERATOR",
      "scope": { "type": "ORG_UNIT", "id": "UNIT-B" }
    }
  ]
}

وقتی نقش و محدوده صریح شدند، پرسش بعدی معماری است: تصمیم کجا گرفته می‌شود؟ من از PEP، PDP و PIP به عنوان مسئولیت‌های منطقی استفاده می‌کنم؛ نه با این ادعا که الزاماً سرویس‌هایی با استقرار جداگانه هستند.

نقاط اعمال و تصمیم سیاست و اطلاعات معتبر

یک سرویس می‌تواند از نظر فیزیکی چند مسئولیت را میزبانی کند. تفکیک مفهومی است تا مالکیت مسئولیت‌ها روشن بماند.

PEP؛ نقطه اعمال سیاست

عملیات را دریافت می‌کند و نتیجه اجازه یا رد را اعمال می‌کند. بررسی در درگاه می‌تواند کلی باشد؛ عملیات روی منبع، به نقطه اعمالی نیاز دارد که دامنه را بشناسد.

PDP؛ نقطه تصمیم سیاست

مجوز لازم، تخصیص فعال نقش، پوشش محدوده و محدودیت‌های زمان اجرا را ارزیابی می‌کند.

PIP؛ نقطه اطلاعات سیاست

واقعیت‌های معتبر، مانند مالک سازمانی واقعی منبع، وضعیت گردش کار، ایجادکننده یا اطلاعات تفویض اختیار را فراهم می‌کند.

مرز مهم: request.unitId ورودی است، نه مدرک. پیش از تطبیق محدوده، منبع محافظت‌شده باید از یک مرجع معتبر بازیابی شود.

مسئولیت هر لایه چیست

هر مرحله را انتخاب کنید. این تفکیک مانع تبدیل درگاه به موتور سیاست دامنه و مانع اعتماد سرویس‌ها به زمینه تأییدنشده کاربر می‌شود.

هویت

منبع معتبر، هویت، طبقه‌بندی شغلی و جایگاه سازمانی شخص را فراهم می‌کند. این‌ها ورودی ایجاد تخصیص هستند، نه مجوز نقطه پایانی.

تصمیم نهایی صرفاً این نیست که «آیا توکن شامل APPROVE است؟» بلکه به این مجموعه شرط نزدیک‌تر است:

بررسی شرط لازم
توکن validToken
قابلیت op ∈ permissions(assignment.role)
محدوده covers(assignment.scope, scopeOf(resource))
وضعیت workflowAllows(op, resource.state)
تفکیک وظایف principal ≠ forbiddenActor(resource)
// Never authorize against a client assertion
unsafe = request.unitId == token.scopeId

// Resolve the protected resource first
resource    = repository.load(request.resourceId)
actualScope = scopeOf(resource)

allow =
  validToken(token)
  && exists activeAssignment where
       grants(activeAssignment.role, operation)
       && covers(activeAssignment.scope, actualScope)
  && runtimeRules(principal, operation, resource)

در همین مرز، مجوزدهی از مسئله‌ای درباره توکن ایستا به مسئله تازگی اطلاعات تبدیل می‌شود. JWT امضاشده یک تصویر لحظه‌ای است. اگر تخصیصی پس از صدور توکن لغو شود، یک توکن کاملاً خودبسنده به‌تنهایی از این تغییر مطلع نمی‌شود.

یک JWT معتبر می‌تواند مجوزهای قدیمی داشته باشد

تأخیر قابل قبول در اعمال لغو دسترسی باید یک تصمیم صریح معماری باشد.

  • T0؛ تخصیص فعال است: توکن همراه با تخصیص نقش محدوده‌دار صادر می‌شود.
  • T1؛ تخصیص لغو می‌شود: وضعیت معتبر مجوزدهی تغییر می‌کند.
  • T2؛ توکن قدیمی ارائه می‌شود: امضا و زمان انقضای آن ممکن است همچنان معتبر باشند.
  • T3؛ کنترل تازگی: عمر کوتاه، بررسی نسخه، استعلام یا حافظه نهان آگاه از لغو، مدت بقای دسترسی قدیمی را محدود می‌کند.

کنترل‌های ممکن

  • توکن دسترسی کوتاه‌عمر: بازه بقای دسترسی قدیمی را محدود می‌کند.
  • نسخه مجوزدهی: توکن‌های قدیمی‌تر از نسخه فعلی سیاست شخص را رد می‌کند.
  • استعلام یا PDP مرکزی: تصمیم تازه‌تر را با هزینه وابستگی در زمان اجرا فراهم می‌کند.
  • حافظه نهان آگاه از لغو: مراجعه تکراری را کاهش می‌دهد و معنای بی‌اعتبارسازی را حفظ می‌کند.

من این موازنه را پشت عبارت «JWT بدون وضعیت است» پنهان نمی‌کنم. اعتبارسنجی بدون وضعیت یک ویژگی پیاده‌سازی است؛ تأخیر لغو، یک نیازمندی امنیتی. سازوکار مناسب به سرعت لازم برای اعمال تغییر دسترسی و میزان وابستگی قابل تحمل در زمان اجرا بستگی دارد.

همین انضباط در آزمون هم کاربرد دارد. قواعد مجوزدهی برای آزمون‌های خودکار قطعی مناسب‌اند؛ زیرا هر قاعده را می‌توان در قالب یک نمونه تصمیم کوچک بیان کرد.

مسیر مجوزدهی؛ چرا درخواست پذیرفته یا رد شد

یک سناریو انتخاب کنید. مجموعه آزمون عملیاتی باید هم تصمیم و هم شرط شکست‌خورده را بررسی کند؛ نه فقط کد وضعیت HTTP را.

نقش معتبر و محدوده منطبقاجازه
  1. ✓
    توکن

    امضا و زمان انقضا معتبرند

  2. ✓
    تخصیص

    تخصیص فعال و در بازه اعتبار است

  3. ✓
    مجوز

    نقش همان تخصیص، عملیات تأیید را مجاز می‌داند

  4. ✓
    محدوده

    همان تخصیص محدوده واقعی منبع را پوشش می‌دهد

  5. ✓
    گردش کار

    منبع در وضعیت قابل تأیید است

  6. ✓
    تفکیک وظایف

    شخص، ایجادکننده منبع نیست

من عمداً از RBAC مستقیماً به یک مدل کاملاً عمومی ABAC یا گراف روابط نرفتم. مسئله، قابلیت‌های عملکردی پایدار، مرزهای سازمانی صریح و تعداد محدودی محدودیت زمان اجرا داشت. تخصیص محدوده‌دار این ساختار را حل کرد، بدون اینکه تمام سیاست‌ها را به یک زبان عمومی منتقل کند.

چرا RBAC محدوده‌دار پیچیدگی متناسبی داشت

وقتی مسئله واقعاً نیاز دارد، مدل غنی‌تر انتخاب کنید؛ نه صرفاً به دلیل جدیدتر بودن واژگان آن.

RBAC محدوده‌دار

وقتی قابلیت‌ها پایدار، محدوده سازمانی صریح و محدودیت‌های زمان اجرا محدود و وابسته به دامنه‌اند، انتخاب مناسبی است.

ABAC

وقتی مجوزدهی به تعداد زیادی ویژگی مستقل و متغیر از شخص، منبع و محیط وابسته است، جذاب‌تر می‌شود.

ReBAC

وقتی دسترسی اساساً یک مسئله گراف است، مانند مالکیت، تفویض، عضویت، اشتراک‌گذاری یا روابط تو‌در‌تو، جذاب‌تر می‌شود.

راه‌حل اصلی پیچیده‌تر کردن مدل نقش نبود؛ انتقال پیچیدگی به ابعادی بود که واقعاً به آن‌ها تعلق داشت.

این کار فهرست نقش‌ها را قابل استفاده مجدد نگه می‌دارد، جابه‌جایی سازمانی را به تغییر داده تبدیل می‌کند، معنای دقیق پیوند نقش و محدوده را در توکن حفظ می‌کند، مرجع معتبر منبع را در اختیار سرویس می‌گذارد و سیاست نهایی را قابل استدلال و آزمون می‌کند.

قاعده‌ای که از این طراحی نگه داشتم

وقتی مدل RBAC نقش‌هایی تولید می‌کند که هم‌زمان چه کسی، چه کاری، کجا و چه زمانی را توصیف می‌کنند، احتمالاً پاسخ یک نقش دیگر نیست. یکی از این ابعاد به جای دیگری تعلق دارد.