27 KiB
مستند کامل سیستم Access Control
این سند معماری، اجزای اصلی، جریان اجرا، مدل داده، تنظیمات، APIها و محدودیت های فعلی سیستم access_control را در بک اند CropLogic توضیح می دهد.
هدف سیستم
سیستم access_control برای پاسخ به این سوال طراحی شده است:
- آیا یک کاربر روی یک مزرعه مشخص به یک قابلیت خاص دسترسی دارد یا نه؟
- این تصمیم بر چه اساسی گرفته می شود: پلن اشتراک، نوع مزرعه، محصول، سنسور، یا قوانین اختصاصی؟
- آیا این تصمیم باید در سطح کل route اعمال شود یا در سطح یک feature مشخص داخل view؟
این سیستم دو لایه اصلی دارد:
- کنترل دسترسی در سطح route با
RouteFeatureAccessMiddleware - کنترل دسترسی در سطح feature/view با
FeatureAccessPermission
هسته تصمیم گیری نهایی هم از طریق سرویس OPA انجام می شود و بک اند نقش جمع آوری context، ارسال درخواست، cache کردن نتیجه و اعمال پاسخ را دارد.
محل های اصلی پیاده سازی
فایل های مهم این سیستم:
access_control/models.pyaccess_control/services.pyaccess_control/permissions.pyaccess_control/middleware.pyaccess_control/views.pyaccess_control/serializers.pyaccess_control/urls.pyconfig/feature.jsonconfig/settings.py
فعال سازی سراسری در تنظیمات:
config/settings.py:75-> اضافه شدنaccess_control.middleware.RouteFeatureAccessMiddlewareconfig/settings.py:145-> اضافه شدنaccess_control.permissions.FeatureAccessPermissionبهDEFAULT_PERMISSION_CLASSES
این یعنی به صورت پیش فرض، تمام endpointهای DRF که anonymous نیستند، هم از نظر authentication و هم از نظر access control بررسی می شوند.
اجزای دامنه داده
1) SubscriptionPlan
مدل SubscriptionPlan در access_control/models.py پلن اشتراک را نگه می دارد.
فیلدهای مهم:
code: کد یکتا مثلgoldیاstartername: نام نمایشی پلنmetadata: داده های تکمیلی؛ مثلا تعیین پلن پیش فرض با{"is_default": true}is_active: فعال یا غیرفعال بودن پلن
کاربرد اصلی:
- اتصال مستقیم به
FarmHub.subscription_plan - استفاده در match شدن ruleها
- fallback برای مزارعی که پلن ندارند
2) AccessFeature
مدل AccessFeature قابلیت هایی را تعریف می کند که باید درباره آن ها تصمیم گیری شود.
فیلدهای مهم:
code: شناسه یکتای feature مثلfarm_dashboardname: نام خواناfeature_type: یکی ازpage،widgetیاactiondefault_enabled: وضعیت پیش فرض قبل از اعمال ruleهاmetadata: داده های توسعه پذیر
نکته مهم:
default_enabled نقطه شروع محاسبه است. بعد از آن ruleها می توانند وضعیت هر feature را تغییر دهند.
3) AccessRule
مدل AccessRule قانون های دسترسی را نگه می دارد.
فیلدهای مهم:
code: شناسه یکتا برای rulepriority: اولویت اجرا؛ عدد کمتر یعنی زودتر پردازش می شودeffect: یکی ازallowیاdenyfeatures: featureهایی که rule روی آن ها اثر می گذاردsubscription_plans: محدودیت بر اساس پلنfarm_types: محدودیت بر اساس نوع مزرعهproducts: محدودیت بر اساس محصولsensor_catalogs: محدودیت بر اساس کاتالوگ دستگاه/سنسورmetadata: برای شرط های تکمیلی مثلsensor_catalog_codes
نکته رفتاری:
در build_farm_access_profile ruleها بر اساس priority و سپس id مرتب می شوند. اگر چند rule روی یک feature اثر بگذارند، ruleهای بعدی می توانند نتیجه قبلی را override کنند.
4) FarmAccessProfile
مدل FarmAccessProfile snapshot یا خروجی resolved شده دسترسی های یک مزرعه است.
فیلدهای مهم:
farm: ارتباط one-to-one باFarmHubsubscription_plan: پلنی که در نهایت برای resolve استفاده شدهprofile_data: خروجی نهایی شامل featureها و matched ruleهاresolved_from_profile: فلگ کمکی
این مدل بیشتر برای materialize کردن وضعیت دسترسی مزرعه استفاده می شود تا بتوان نتیجه محاسبه را ذخیره و بازاستفاده کرد.
ارتباط با FarmHub
در farm_hub/models.py، هر مزرعه (FarmHub) این contextها را دارد:
ownerfarm_typesubscription_planproductssensors
سیستم access control از همین context برای تصمیم گیری استفاده می کند. بنابراین دسترسی صرفا به user وابسته نیست؛ بلکه به ترکیب user + farm + ویژگی های مزرعه وابسته است.
منطق پلن پیش فرض
تابع های مرتبط:
get_default_subscription_planget_effective_subscription_plan
منطق به این صورت است:
- اگر خود مزرعه
subscription_planداشته باشد، همان استفاده می شود. - اگر نداشته باشد، اولین پلن فعال با
metadata.is_default=Trueانتخاب می شود. - اگر چنین پلنی هم نبود، پلن
goldازaccess_control/catalog.pyfallback می شود.
این رفتار باعث می شود مزرعه بدون پلن صریح هم قابل authorize باشد.
ساخت Access Profile داخل بک اند
تابع اصلی: build_farm_access_profile در access_control/services.py
ورودی
یک آبجکت FarmHub
مراحل اجرا
- مزرعه با
select_relatedوprefetch_relatedکامل reload می شود. - پلن موثر مزرعه با
get_effective_subscription_planتعیین می شود. - محصول های مزرعه و device catalogهای سنسورهای مزرعه جمع آوری می شوند.
- تمام
AccessFeatureهای فعال خوانده می شوند. - وضعیت اولیه هر feature از
default_enabledساخته می شود. - تمام
AccessRuleهای فعال، به ترتیبpriorityبررسی می شوند. - هر rule با تابع
_match_ruleروی مزرعه match می شود. - اگر rule match شود:
- به
matched_rulesاضافه می شود - featureهای مرتبط را
allowیاdenyمی کند sourceآن feature برابر کد rule می شود
- به
- نتیجه نهایی در
FarmAccessProfileذخیره می شود. - خروجی profile به caller برگردانده می شود.
شرط های match شدن rule
تابع _match_rule این موارد را بررسی می کند:
- فعال بودن rule
- سازگاری پلن اشتراک
- سازگاری نوع مزرعه
- وجود حداقل یک product منطبق
- وجود حداقل یک sensor catalog منطبق
- وجود تقاطع با
metadata["sensor_catalog_codes"]
ساختار تقریبی خروجی profile
{
"farm_uuid": "...",
"subscription_plan": {
"uuid": "...",
"code": "gold",
"name": "Gold"
},
"features": {
"farm_dashboard": {
"name": "Farm Dashboard",
"type": "page",
"enabled": true,
"source": "starter-dashboard-rule"
}
},
"matched_rules": [
{
"code": "starter-dashboard-rule",
"name": "Starter Dashboard Rule",
"effect": "allow",
"priority": 10
}
],
"resolved_from_profile": true
}
این خروجی بیشتر برای نمایش، debugging، یا ساخت viewهای profile-based مناسب است.
کنترل دسترسی runtime با OPA
ایده کلی
در زمان درخواست API، بک اند خودش ruleها را برای همه routeها نهایی نمی کند؛ بلکه context را به سرویس OPA می فرستد و جواب allow/deny می گیرد.
چرا OPA؟
مزایا:
- جدا شدن policy از کد بک اند
- امکان تغییر ruleها بدون بازنویسی منطق اصلی viewها
- مناسب برای تصمیم گیری policy-based و context-aware
تنظیمات OPA
در config/settings.py:
ACCESS_CONTROL_AUTHZ_ENABLEDACCESS_CONTROL_AUTHZ_BASE_URLACCESS_CONTROL_AUTHZ_BATCH_PATHACCESS_CONTROL_AUTHZ_TIMEOUTACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT
مقدار پیش فرض base URL:
http://croplogic-accsess-opa:8181
مسیر پیش فرض batch authorization:
/v1/data/croplogic/authz/batch_decision
ساخت payload برای OPA
تابع های مهم:
build_opa_userbuild_opa_resourcebuild_authorization_input
build_opa_user
اطلاعات user را به فرم policy-friendly تبدیل می کند:
idusernameemailphone_numberis_staffis_superuserroleکه فعلا ثابت و برابرfarmerاست
build_opa_resource
اطلاعات مزرعه را به فرم resource برمی گرداند. اگر مزرعه وجود نداشته باشد، فیلدها با مقدارهای خالی برگردانده می شوند.
فیلدهای مهم resource:
farm_idsubscription_plan_codesfarm_typescrop_typessensor_codespower_sensorcustomization
نکته:
power_sensor از sensor.power_source.type استخراج می شود، اگر این ساختار به صورت dict ذخیره شده باشد.
build_authorization_input
payload نهایی برای OPA را می سازد:
{
"user": {"...": "..."},
"resource": {"...": "..."},
"features": ["farm_dashboard", "notifications"],
"action": "view",
"route": "/api/dashboard/"
}
درخواست batch به OPA
تابع اصلی: request_opa_batch_authorization
رفتار
- اگر
ACCESS_CONTROL_AUTHZ_ENABLED=falseباشد، برای همه featureها مقدارtrueبرمی گرداند. - اگر لیست feature خالی باشد، پاسخ خالی برمی گرداند.
- در غیر این صورت با
requests.postبه OPA درخواست می فرستد. - payload داخل کلید
inputارسال می شود. - نتیجه از
response.json().get("result", {})خوانده می شود.
Error handling
اگر ارتباط با OPA fail شود:
- exception از نوع
requests.RequestExceptionگرفته می شود - event لاگ می شود
- metric ثبت می شود
- خطای
AccessControlServiceUnavailableبالا انداخته می شود
اگر OPA JSON نامعتبر برگرداند:
- metric
access_control.opa.invalid_jsonثبت می شود AccessControlServiceUnavailableبرگردانده می شود
اگر result خالی باشد:
- metric
access_control.opa.empty_resultثبت می شود - warning لاگ می شود
فرمت های پاسخ قابل قبول از OPA
تابع normalize_opa_batch_result چند نوع payload را پشتیبانی می کند:
حالت 1: decisions
{
"decisions": {
"farm_dashboard": true,
"notifications": false
}
}
حالت 2: features با ساختار nested
{
"features": {
"farm_dashboard": {"allow": true},
"notifications": {"allow": false}
}
}
حالت 3: allowed_features
{
"allowed_features": ["farm_dashboard"]
}
حالت 4: map ساده از booleanها
{
"farm_dashboard": true,
"notifications": false
}
اگر payload خارج از این الگوها باشد، سیستم آن را unsupported تلقی می کند و AccessControlServiceUnavailable می دهد.
لایه cache
تابع اصلی: batch_authorize_features
چرا cache داریم؟
برای جلوگیری از درخواست تکراری به OPA در routeهای پرترافیک.
کلید cache چگونه ساخته می شود؟
تابع _get_authorization_cache_key این داده ها را hash می کند:
farm_uuiduser_idfeaturesبه صورت sortedactionroute
سپس خروجی SHA-256 با prefix زیر ذخیره می شود:
access-control:authz:<sha256>
زمان انقضا
از ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT می آید که پیش فرض آن 300 ثانیه است.
رفتار خطا در cache
اگر خواندن یا نوشتن cache fail شود:
- خطا swallow می شود
- warning و metric observability ثبت می شود
- authorization ادامه پیدا می کند
پس cache optimization است، نه dependency حیاتی.
نگاشت method به action
تابع get_authorization_action از ACTION_MAP استفاده می کند:
GET,HEAD,OPTIONS->viewPOST->createPUT,PATCH->editDELETE->delete
این action در payload ارسالی به OPA قرار می گیرد.
کنترل دسترسی در سطح Route
پیاده سازی در access_control/middleware.py
کلاس: RouteFeatureAccessMiddleware
هدف
قبل از رسیدن request به view، بر اساس app مربوط به route، یک feature_code سراسری پیدا کند و اجازه دسترسی را چک کند.
منبع feature_code برای route
از فایل config/feature.json استفاده می شود.
مثلا:
account->account_managementfarm_hub->farm_managementdashboard->farm_dashboardnotifications->notifications
جریان اجرا
- اگر
view_classوجود نداشته باشد، middleware کاری نمی کند. - اگر view،
AllowAnyداشته باشد، middleware عبور می دهد. - اگر user هنوز authenticate نشده باشد، با JWT تلاش به authenticate می کند.
- از روی نام app در ماژول view،
feature_coderoute پیدا می شود. - اگر mapping وجود نداشته باشد، request بدون این check عبور می کند.
- اگر
farm_uuidدر path/query/body باشد، مزرعه متعلق به user لود می شود. - تابع
authorize_featureبا context فعلی صدا زده می شود. - اگر deny شود، پاسخ
403برمی گرداند. - اگر OPA unavailable باشد، پاسخ
503برمی گرداند. - اگر مجاز باشد، مقدار
request.route_feature_codeست می شود.
استخراج farm_uuid
middleware به این ترتیب farm_uuid را پیدا می کند:
view_kwargsrequest.GETget_request_data(request)برای body JSON
نکته مهم
در route-level check، اگر route به مزرعه خاصی وابسته نباشد، farm=None به OPA فرستاده می شود. بنابراین policy باید بتواند هر دو حالت farm-aware و farm-less را مدیریت کند.
کنترل دسترسی در سطح View/Feature
پیاده سازی در access_control/permissions.py
کلاس: FeatureAccessPermission
هدف
برای viewهایی که یک feature خاص لازم دارند، علاوه بر route-level feature، یک feature جزئی تر هم بررسی شود.
نحوه فعال شدن
هر view می تواند property زیر را تعریف کند:
required_feature_code = "some_feature_code"
اگر این property وجود نداشته باشد، این permission به صورت خودکار True برمی گرداند.
جریان اجرا
required_feature_codeاز view خوانده می شود.farm_uuidازkwargs،query_paramsیا body گرفته می شود.- مزرعه با شرط
owner=request.userلود می شود. authorize_featureبرای همان feature خاص اجرا می شود.- در صورت deny، پیام
Access to feature ... is denied.تنظیم می شود. - در صورت unavailable بودن OPA، متن exception به عنوان message برمی گردد.
تفاوت با middleware
- middleware بر اساس
app -> route featureتصمیم می گیرد. - permission بر اساس
required_feature_codeهر view تصمیم می گیرد.
پس ممکن است یک request از middleware عبور کند، ولی در permission لایه دوم رد شود.
استخراج body برای authorization
تابع get_request_data برای این ساخته شده که حتی قبل از parse کامل DRF هم بتوان farm_uuid را از request پیدا کرد.
رفتار
- اگر
request.dataاز قبل dict یاQueryDictباشد، همان برگردانده می شود. - اگر body خالی باشد،
{}برمی گردد. - اگر
content-typeبرابرapplication/jsonباشد، body parse می شود. - فقط اگر خروجی یک dict باشد cache می شود و برگردانده می شود.
- اگر parse خطا بدهد،
{}برمی گردد.
این رفتار مخصوصا برای middleware مهم است، چون آنجا همیشه request.data آماده نیست.
API موجود در access_control
در حال حاضر route رسمی app این است:
POST /api/access-control/farms/<farm_uuid>/authorize/
تعریف آن در access_control/urls.py آمده است.
View مربوطه
FarmFeatureAuthorizationView در access_control/views.py
ورودی
serializer:
{
"features": ["farm_dashboard", "notifications"],
"action": "view"
}
featuresاجباری و non-empty استactionاختیاری است و پیش فرض آنviewاست
رفتار endpoint
- کاربر باید authenticate باشد.
- مزرعه با
farm_uuidوowner=request.userپیدا می شود. - request مستقیم به
request_opa_batch_authorizationمی رود. - پاسخ OPA بدون normalize شدن کامل، داخل
decisionبرگردانده می شود.
پاسخ موفق
ساختار تقریبی:
{
"code": 200,
"msg": "success",
"data": {
"farm_uuid": "...",
"user": {
"id": 1,
"username": "user",
"email": "user@example.com",
"phone_number": "0912..."
},
"features": ["farm_dashboard"],
"action": "view",
"decision": {
"decisions": {
"farm_dashboard": true
}
}
}
}
پاسخ های خطا
404: مزرعه پیدا نشد503: سرویس OPA در دسترس نیست
نگاشت app به feature در config/feature.json
این فایل تعیین می کند هر app در سطح route با چه featureی کنترل شود.
نمونه های فعلی:
auth->auth_accessaccount->account_managementfarm_hub->farm_managementdashboard->farm_dashboardirrigation->irrigationfertilization->fertilizationnotifications->notificationsaccess_control->access_control
نکته طراحی
این mapping coarse-grained است؛ یعنی در سطح app عمل می کند، نه در سطح تک endpoint. اگر نیاز به granularity بیشتر باشد، باید یا:
- routeها app-level جدا شوند
- یا روی viewها
required_feature_codeهای ریزتر تعریف شود
تعامل با authentication
- authentication پیش فرض پروژه JWT است.
- middleware اگر
request.userآماده نباشد، خودش باJWTAuthenticationتلاش می کند کاربر را از روی token شناسایی کند. - اگر token نامعتبر باشد، middleware سکوت می کند و authentication را به جریان عادی DRF واگذار می کند.
این طراحی باعث می شود middleware برای کاربر ناشناس تصمیم اشتباه نگیرد و با لایه auth اصلی conflict نداشته باشد.
رفتار با viewهای AllowAny
در middleware، اگر view داخل permission_classes از AllowAny استفاده کند، route-level access control روی آن route اعمال نمی شود.
این رفتار برای endpointهای عمومی مثل login، register، یا webhookها لازم است.
observability و metrics
در access_control/services.py چند جای مهم instrumentation وجود دارد:
observe_operation(...)برای اندازه گیری عملیات batch authorizationlog_event(...)برای ثبت خطاها و warningهاrecord_metric(...)برای شمارنده هایی مثل:access_control.opa.failureaccess_control.opa.invalid_jsonaccess_control.opa.empty_result
این داده ها برای monitoring کیفیت ارتباط با OPA مهم هستند.
تست های موجود
تست های اصلی در access_control/tests.py و بخشی در farm_hub/tests.py قرار دارند.
مواردی که الان تست شده اند:
- cache شدن authorization برای route یکسان
- وجود
routeدر payload ارسالی به OPA - پشتیبانی از payload nested در
normalize_opa_batch_result - ثبت metric هنگام invalid JSON از OPA
- ارسال
feature_codeوactionدرست از middleware - resolve شدن profile بر اساس چند rule مختلف
- fallback شدن subscription plan به پلن پیش فرض
این تست ها نشان می دهند که سیستم هم لایه runtime authorization و هم لایه profile resolution را پوشش داده است.
محدودیت ها و نکات مهم فعلی
1) route-level mapping در سطح app است
اگر یک app چند endpoint با سطح دسترسی متفاوت داشته باشد، config/feature.json به تنهایی کافی نیست و باید از required_feature_code یا سیاست های دقیق تر استفاده شود.
2) نقش کاربر فعلا ساده است
در build_opa_user مقدار role فعلا همیشه farmer است. اگر در آینده نقش هایی مثل admin، agronomist یا support اضافه شوند، این بخش باید واقعی تر شود.
3) profile builder و runtime OPA دو مسیر متفاوت دارند
build_farm_access_profileruleها را داخل خود Django resolve می کند.- runtime authorization از OPA جواب می گیرد.
اگر policyهای OPA و داده های rule داخل Django از هم فاصله بگیرند، ممکن است اختلاف رفتار ایجاد شود. بنابراین باید policyها و rule modelها هماهنگ نگه داشته شوند.
4) endpoint رسمی profile در app دیده نمی شود
در کد فعلی access_control/urls.py فقط endpoint authorize ثبت شده است. پس اگر قرار باشد profile نهایی برای فرانت نمایش داده شود، یا باید endpoint جدید ساخته شود یا از سرویس build_farm_access_profile در view دیگری استفاده شود.
5) migrationهای seed فعلا خالی هستند
فایل های 0003_seed_default_access_rules.py و 0004_enable_default_feature_access.py در وضعیت فعلی عملیات migration ندارند. یعنی seed اولیه ruleها و featureها احتمالا هنوز به صورت migration-based پیاده نشده یا بعدا منتقل شده است.
سناریوی کامل اجرای یک request
فرض کنیم کاربر درخواست زیر را می زند:
PATCH /api/account/profile/
جریان کلی:
- request وارد middleware می شود.
- middleware تشخیص می دهد app این route برابر
accountاست. - از
config/feature.json، feature برابرaccount_managementپیدا می شود. - method برابر
PATCHاست، پس action می شودedit. - context کاربر و مزرعه احتمالی جمع آوری می شود.
- درخواست batch به OPA ارسال می شود.
- اگر
account_managementمجاز باشد، request به DRF می رسد. - DRF authentication/permissionهای عادی را هم بررسی می کند.
- اگر view یک
required_feature_codeاضافی داشته باشد،FeatureAccessPermissionدوباره access را بررسی می کند. - اگر همه چیز مجاز باشد، business logic view اجرا می شود.
راهنمای توسعه و افزودن قابلیت جدید
اگر بخواهید یک feature جدید به سیستم اضافه کنید، این مراحل پیشنهاد می شود:
افزودن feature جدید
- یک
AccessFeatureجدید باcodeمناسب بسازید. - اگر لازم است route-level باشد، app مربوطه را در
config/feature.jsonmap کنید. - اگر granular است، روی view مقدار
required_feature_codeتعریف کنید. - policy متناظر را در OPA اضافه یا به روز کنید.
- اگر profile-based resolution هم مهم است، ruleهای Django را هم اضافه کنید.
- تست middleware/permission/profile را اضافه کنید.
افزودن rule جدید
AccessRuleبسازید.- featureها را به آن وصل کنید.
- شرط ها را با یکی یا چند مورد از این ها تنظیم کنید:
- subscription plan
- farm type
- product
- sensor catalog
- metadata مثل
sensor_catalog_codes
priorityرا دقیق انتخاب کنید تا overrideها قابل پیش بینی باشند.
جمع بندی
سیستم access_control در این پروژه یک لایه دسترسی چندبعدی است که:
- بر پایه user و farm تصمیم می گیرد
- پلن اشتراک، نوع مزرعه، محصول و سنسور را وارد تصمیم می کند
- در سطح route و feature قابل اعمال است
- تصمیم runtime را به OPA واگذار می کند
- برای performance از cache استفاده می کند
- برای profile/resolution داخلی، snapshot قابل ذخیره می سازد
اگر بخواهیم خیلی خلاصه بگوییم:
models.pyتعریف می کند چه چیزهایی روی access اثر دارندservices.pycontext را می سازد، OPA را صدا می زند و نتیجه را normalize می کندmiddleware.pyروی routeها gatekeeper استpermissions.pyروی featureهای ریزتر gatekeeper استfeature.jsonmapping سطح app را مشخص می کندsettings.pyکل سیستم را فعال و تنظیم می کند