> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# OpenFGA عملياً: نموذج العلاقات من التصميم إلى الإنتاج
- URL: https://arabroot.io/articles/openfga-عمليا-نموذج-العلاقات-من-التصميم-الى-الانتاج/
- Published: 2026-10-12T23:45:00.000Z
- Updated: 2026-10-12T15:51:43.000Z
- Description: كيف تصمم نموذج علاقات في OpenFGA للمؤسسة والفرق والمجلدات، وتختبره وتغيره وتشغله على PostgreSQL دون مفاجآت.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, الاستضافة الذاتية

في [المقال الأول من السلسلة](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/#lab-openfga-model) كتبنا نموذجاً صغيراً في OpenFGA وسألناه بأمر check، وفي [منصة صلاحيات مركزية لخدماتك](https://arabroot.io/articles/%D9%85%D9%86%D8%B5%D8%A9-%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-microservices/) جعلناه نقطة قرار لعدة خدمات وقسنا زمنه واتساقه. وفي هذا المقال نترك المنصة وننظر داخل المحرك نفسه، لأن أغلب المشكلات التي تقابلها بعد أشهر من التشغيل لا تأتي من الشبكة ولا من الخدمات، وإنما من النموذج: علاقة صممت بشكل يعطي صلاحية أوسع مما تريد، أو شرط ينقصه سياق، أو نموذج جديد نشر فغير القرارات دون أن يلاحظ أحد.

المواضيع في السلسلة:

- [الصلاحيات المركزية Authorization](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/): النماذج والمصطلحات والمحركات.
- [منصة صلاحيات مركزية لخدماتك](https://arabroot.io/articles/%D9%85%D9%86%D8%B5%D8%A9-%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-microservices/): المنصة والمزامنة والاتساق.
- OpenFGA عملياً (الموضوع الحالي).
- [OPA عملياً](https://arabroot.io/articles/opa-%D8%B9%D9%85%D9%84%D9%8A%D8%A7-%D8%B3%D9%8A%D8%A7%D8%B3%D8%A7%D8%AA-abac-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-%D9%88%D8%A8%D9%88%D8%A7%D8%A8%D8%A7%D8%AA%D9%83/): سياسات الخصائص ABAC.

وسوف نناقش في هذا المقال ما يلي:

- تشغيل OpenFGA على PostgreSQL بمفتاح للواجهة ومقاييس، وما الجداول التي ينشئها.
- تصميم نموذج لمؤسسة وفرق ومجلدات متداخلة ومستندات، مع الوراثة والاستثناء بالحجب، وصلاحية مؤقتة بشرط Condition.
- العلاقات السياقية Contextual Tuples، ومتى تستخدمها، ولماذا لا تقبلها من المتصفح.
- الأسئلة العكسية ListObjects وListUsers، والاختبارات التي تعمل في CI.
- تغيير النموذج: لماذا كل نموذج جديد له معرف جديد، وكيف فتح حذف علاقة واحدة مستنداً محجوباً في تجربتنا.
- ترقية OpenFGA ونسخه الاحتياطي، والمكتبة الرسمية بلغة Python، والمراقبة.

## OpenFGA على PostgreSQL، ومفتاح للواجهة

قاعدة البيانات في الذاكرة التي استخدمناها في المقال الأول تختفي عند إعادة التشغيل، أما في الإنتاج فيحفظ OpenFGA العلاقات والنماذج في PostgreSQL أو MySQL أو SQLite كما في [دليل التشغيل الرسمي](https://openfga.dev/docs/getting-started/setup-openfga/docker?ref=arabroot.io). نفذنا التجربة على Docker Engine 29.8.0 بإصدار OpenFGA 1.21.0 أولاً ثم رقيناه إلى 1.22.0 في قسم الترقية، مع fga CLI 0.8.1 وPostgreSQL 18.6، وكل ما ننشئه يبدأ اسمه بالبادئة `lab-authz-fga`. نبدأ بملف `.env`:

```bash
mkdir -p ~/openfga-lab && cd ~/openfga-lab
```

```ini
# Lab-only secrets. On a real server: openssl rand -hex 32
PG_PASS=lab-authz-fga-pg-71d3
FGA_KEY=lab-authz-fga-key-2b8e6c0a9f
# The upgrade step changes this to v1.22.0
FGA_VERSION=v1.21.0
```

ثم `compose.yaml`:

```yaml
# OpenFGA on PostgreSQL with an API key, metrics and the CLI image for tests.
name: lab-authz-fga
services:
  pg:
    image: postgres:18.6
    container_name: lab-authz-fga-pg
    environment: { POSTGRES_USER: openfga, POSTGRES_PASSWORD: "${PG_PASS}", POSTGRES_DB: openfga }
    volumes: [pg-data:/var/lib/postgresql]
    healthcheck: { test: ["CMD", "pg_isready", "-U", "openfga"], interval: 3s, retries: 20 }

  migrate:
    image: openfga/openfga:${FGA_VERSION}
    container_name: lab-authz-fga-migrate
    command: migrate
    environment: &ds
      OPENFGA_DATASTORE_ENGINE: postgres
      OPENFGA_DATASTORE_URI: "postgres://openfga:${PG_PASS}@lab-authz-fga-pg:5432/openfga?sslmode=disable"
    depends_on: { pg: { condition: service_healthy } }

  openfga:
    image: openfga/openfga:${FGA_VERSION}
    container_name: lab-authz-fga
    command: run
    environment:
      <<: *ds
      OPENFGA_AUTHN_METHOD: preshared
      OPENFGA_AUTHN_PRESHARED_KEYS: "${FGA_KEY}"
      OPENFGA_LOG_FORMAT: json
      OPENFGA_METRICS_ENABLE_RPC_HISTOGRAMS: "true"
      OPENFGA_DATASTORE_METRICS_ENABLED: "true"
    ports:
      - "127.0.0.1:18180:8080"   # HTTP API
      - "127.0.0.1:18212:2112"   # Prometheus metrics
    depends_on: { migrate: { condition: service_completed_successfully } }

volumes:
  pg-data:
    name: lab-authz-fga-pg-data
networks:
  default:
    name: lab-authz-fga-net
```

في الإعداد أعلاه لاحظ التالي:

- الخدمة `migrate` تشغل الأمر `openfga migrate` الذي ينشئ الجداول أو يرقيها إلى إصدار البرنامج، والمحرك لا يبدأ إلا بعد نجاحها، وسوف نرى أهمية ذلك عند الترقية.
- الواجهة البرمجية في OpenFGA مفتوحة لأي أحد افتراضياً، فقيمة `--authn-method` الافتراضية هي `none` كما يظهر في `openfga run --help`، لذلك فعلنا المفتاح المشترك `preshared`، وفي الإنتاج تستطيع أن تستخدم `oidc` فيتحقق المحرك من توكنات مزود الهوية نفسه. والسبب أن من يصل إلى واجهة الكتابة يستطيع أن يكتب لنفسه أي علاقة، فيصبح مالكاً لأي مستند.
- المقاييس على المنفذ 2112 بصيغة Prometheus، وفعلنا فيها توزيع زمن الطلبات ومقاييس قاعدة البيانات.
- لا يوجد هنا منفذ للواجهة الرسومية Playground، فهي منذ الإصدار 1.14.0 الصادر في 3 أبريل 2026 معطلة افتراضياً ومعلنة كميزة ستزال، بعد الثغرة [GHSA-68m9-983m-f3v5](https://github.com/openfga/openfga/security/advisories/GHSA-68m9-983m-f3v5?ref=arabroot.io) كما في [سجل التغييرات](https://github.com/openfga/openfga/blob/main/CHANGELOG.md?ref=arabroot.io)، لذلك نعمل في هذا المقال بأداة سطر الأوامر والملفات.

```bash
docker compose up -d
docker logs lab-authz-fga-migrate 2>&1 | tail -3
docker exec lab-authz-fga-pg psql -U openfga -c '\dt'
curl -s 127.0.0.1:18180/stores; echo
curl -s -H 'Authorization: Bearer lab-authz-fga-key-2b8e6c0a9f' 127.0.0.1:18180/stores; echo
```

```text
INFO	db info	{"current version": 0}
INFO	running all migrations
INFO	migration done
                 List of tables
 Schema |        Name         | Type  |  Owner
--------+---------------------+-------+---------
 public | assertion           | table | openfga
 public | authorization_model | table | openfga
 public | changelog           | table | openfga
 public | goose_db_version    | table | openfga
 public | store               | table | openfga
 public | tuple               | table | openfga
(6 rows)

{"code":"bearer_token_missing","message":"missing bearer token"}
{"stores":[],"continuation_token":""}
```

الجدول `tuple` فيه العلاقات، و`authorization_model` فيه كل نموذج كتبته، و`changelog` سجل كل كتابة وحذف، و`goose_db_version` يحفظ إصدار مخطط قاعدة البيانات الذي وصلت إليه. وأداة سطر الأوامر نشغلها من Docker Image الرسمية بدالة صغيرة في الملف `fga.sh`:

```bash
# fga CLI from its official image, on the lab network, with the API key from .env
fga() { docker run --rm -i --network lab-authz-fga-net -v "$PWD:/work" -w /work \
  -e FGA_API_URL=http://lab-authz-fga:8080 -e FGA_API_TOKEN="$FGA_KEY" -e FGA_STORE_ID="$FGA_STORE_ID" \
  -e FGA_MODEL_ID="$FGA_MODEL_ID" openfga/cli:v0.8.1 "$@"; }
```

## تصميم النموذج: مؤسسة وفريق ومجلدات ومستندات

المتطلبات التي نصممها: المؤسسة acme لها مديرة اسمها noura، وفريق المالية فيه alice وbob، ومجلد المالية يملكه أعضاء الفريق قراءة، وبداخله مجلد 2026 فيه مستندان، والمستند salaries محجوب عن bob تحديداً حتى لو كان في الفريق، ومدقق خارجي اسمه khalid يقرأ مجلد المالية ساعة واحدة فقط. وهذا النموذج `model.fga`:

```text
model
  schema 1.1

type user

type organization
  relations
    define admin: [user]
    define member: [user] or admin

type team
  relations
    define org: [organization]
    define member: [user]

type folder
  relations
    define org: [organization]
    define parent: [folder]
    define owner: [user, team#member]
    define editor: [user, team#member] or owner or editor from parent
    define viewer: [user, team#member, user with not_expired] or editor or viewer from parent
    define can_delete: owner or admin from org or can_delete from parent

type document
  relations
    define folder: [folder]
    define owner: [user]
    define blocked: [user]
    define editor: [user] or owner or editor from folder
    define viewer: ([user, user with not_expired] or editor or viewer from folder) but not blocked
    define can_delete: owner or can_delete from folder

condition not_expired(current_time: timestamp, grant_time: timestamp, grant_duration: duration) {
  current_time < grant_time + grant_duration
}
```

والصورة التالية تبين العلاقات التي سوف نكتبها على هذا النموذج:

![رسم بياني: noura مديرة admin للمؤسسة organization:acme، والمؤسسة org لمجلد folder:finance، وفريق team:finance فيه alice وbob أعضاء member، والفريق يقرأ المجلد viewer عبر team#member، والمجلد folder:finance-2026 أبوه parent هو folder:finance، وفيه المستندان document:budget وdocument:salaries، وbob محجوب blocked عن salaries بعلاقة but not، وkhalid يقرأ folder:finance بشرط not_expired لمدة ساعة من 12:00](https://arabroot.io/content/images/2026/10/openfga-practice-01-model.webp)

لا يوجد سطر واحد يقول إن bob يقرأ budget، والمحرك يستنتجها من الفريق والمجلد الأب

في النموذج أعلاه لاحظ التالي:

- `[user, team#member]` في علاقة `viewer` تعني أن القارئ يمكن أن يكون مستخدماً منفرداً أو كل أعضاء فريق، وال `#member` هنا مجموعة مستخدمين Userset، فإذا أضفت شخصاً إلى الفريق قرأ كل ما يقرؤه الفريق دون أي علاقة جديدة على المجلدات.
- `editor from parent` و`viewer from parent` هي الوراثة عبر المجلدات المتداخلة، وتسمى في التوثيق Tuple to Userset، فمن يقرأ المجلد الأب يقرأ المجلد الابن وكل ما بداخله.
- `can_delete` في المجلد تعطي مديرة المؤسسة حق الحذف، ولكنها لا تعطيها حق القراءة، لأن `viewer` لا يذكر `admin from org`. وهذا فصل مقصود: المديرة تستطيع أن تنظف مجلدات موظف غادر دون أن تقرأ رواتب الجميع، فلا تجعل الصلاحيات الإدارية تشمل القراءة تلقائياً لمجرد أنها أسهل في الكتابة.
- `but not blocked` هو الاستثناء Exclusion: كل من يقرأ عبر أي طريق، إلا من كتبت له علاقة `blocked` على هذا المستند. والأقواس تجمع الطرق الثلاثة في تعبير واحد، ثم يطبق عليه الاستثناء.
- `user with not_expired` علاقة لها شرط Condition مكتوب بلغة CEL في آخر الملف، والعلاقة لا تكون صالحة إلا إذا كان الشرط صحيحاً وقت السؤال، كما في [توثيق الشروط](https://openfga.dev/docs/modeling/conditions?ref=arabroot.io).

ننشئ المخزن Store بالنموذج، ونحفظ المعرفين، ثم نكتب العلاقات من الملف `tuples.yaml`:

```yaml
# The organisation and its finance team
- user: user:noura
  relation: admin
  object: organization:acme
- user: organization:acme
  relation: org
  object: team:finance
- user: user:alice
  relation: member
  object: team:finance
- user: user:bob
  relation: member
  object: team:finance
# Folders: finance belongs to acme, finance-2026 sits inside it
- user: organization:acme
  relation: org
  object: folder:finance
- user: user:alice
  relation: owner
  object: folder:finance
- user: team:finance#member
  relation: viewer
  object: folder:finance
- user: folder:finance
  relation: parent
  object: folder:finance-2026
# Documents
- user: folder:finance-2026
  relation: folder
  object: document:budget
- user: user:alice
  relation: owner
  object: document:budget
- user: folder:finance-2026
  relation: folder
  object: document:salaries
- user: user:bob
  relation: blocked
  object: document:salaries
# An external auditor who may read the finance folder for one hour only
- user: user:khalid
  relation: viewer
  object: folder:finance
  condition:
    name: not_expired
    context:
      grant_time: "2026-10-12T12:00:00Z"
      grant_duration: 1h
```

```bash
set -a; . ./.env; set +a; . ./fga.sh
fga store create --name docs --model model.fga
export FGA_STORE_ID=01M4R1RT6EJCMAQ3P7KBY8K9R3 FGA_MODEL_ID=01M4R1RT71B0915W7XK6HAD76T
fga tuple write --file tuples.yaml
```

```text
{"store":{"created_at":"2026-10-12T12:35:30.639219Z","id":"01M4R1RT6EJCMAQ3P7KBY8K9R3","name":"docs","updated_at":"2026-10-12T12:35:30.639219Z"},"model":{"authorization_model_id":"01M4R1RT71B0915W7XK6HAD76T"}}
{"failed_count":0,"successful":[{"object":"organization:acme","relation":"admin","user":"user:noura"},{"object":"team:finance","relation":"org","user":"organization:acme"},...
```

وفي العلاقة الأخيرة في الملف لاحظ أن الشرط يحمل جزءاً من سياقه: وقت المنح ومدته محفوظان مع العلاقة، أما الوقت الحالي فيرسله التطبيق مع كل سؤال.

## نسأل المحرك

```bash
fga query check user:bob viewer document:budget
fga query check user:bob viewer document:salaries
fga query check user:bob editor document:budget
fga query check user:noura can_delete document:budget
fga query check user:khalid viewer document:budget --context '{"current_time":"2026-10-12T12:30:00Z"}'
fga query check user:khalid viewer document:budget --context '{"current_time":"2026-10-12T13:30:00Z"}'
```

```text
{"allowed":true,"resolution":""}
{"allowed":false,"resolution":""}
{"allowed":false,"resolution":""}
{"allowed":true,"resolution":""}
{"allowed":true,"resolution":""}
{"allowed":false,"resolution":""}
```

bob يقرأ الميزانية عبر فريقه والمجلد الأب، ولا يقرأ الرواتب لأنه محجوب، ولا يحرر الميزانية، وnoura تحذفها، وkhalid يقرؤها الساعة 12:30 ولا يقرؤها الساعة 13:30 لأن ساعته انتهت الساعة 13:00.

### نكسرها: سؤال دون سياق الشرط

ماذا يحدث إذا نسي التطبيق إرسال الوقت؟

```bash
fga query check user:khalid viewer document:budget
fga query list-objects user:khalid viewer document
```

```text
Error: check failed: POST validation error for Check POST with body {"code":"validation_error","message":"failed to evaluate relationship condition: 'not_expired' - tuple 'folder:finance#viewer@user:khalid' is missing context parameters '[current_time]'"}
Error: failed to list objects due to POST validation error for ListObjects POST with body {"code":"validation_error","message":"Internal Server Error"}
```

في المخرج أعلاه لاحظ أن check يقول بالضبط ما الذي ينقص، أما list-objects في الإصدار 1.21.0 فرد برسالة عامة لا تذكر السبب، فإذا رأيت هذه الرسالة في قائمة تستخدم شروطاً فابحث أولاً عن سياق ناقص. والأهم أن الخطأ يجب أن يعامل في تطبيقك كرفض، ولا تقم بإرسال قيمة افتراضية للوقت مثل تاريخ قديم حتى يختفي الخطأ، لأن الوقت القديم يجعل كل صلاحية مؤقتة صالحة إلى الأبد.

ويجدر الإشارة هنا إلى أن الشروط وال Cache اجتمعا في ثغرة [CVE-2026-33729](https://github.com/openfga/openfga/security/advisories/GHSA-h6c8-cww8-35hf?ref=arabroot.io) التي أصلحت في الإصدار 1.13.1 في 24 مارس 2026، حيث كانت طلبات Check مع الشروط وتفعيل ال Cache قد تعيد نتيجة محفوظة غير صحيحة، لذلك تابع إصدارات المحرك مثل أي مكون أمني.

## العلاقات السياقية Contextual Tuples

ليست كل حقيقة يحتاجها القرار مخزنة في المحرك، فعضوية المجموعات مثلاً تأتي في التوكن من مزود الهوية، ونسخها إلى المحرك يعني مزامنة أخرى. والبديل أن ترسلها الخدمة مع السؤال كعلاقة سياقية Contextual Tuple، فيستخدمها المحرك لهذا الطلب فقط ولا يحفظها، كما في [توثيق العلاقات السياقية](https://openfga.dev/docs/modeling/token-claims-contextual-tuples?ref=arabroot.io). والصورة التالية تبين المصادر الثلاثة للحقائق في طلب واحد:

![مخطط: خدمتك تقرأ groups: [finance] من التوكن وترسل طلب Check واحداً إلى OpenFGA فيه user:carol وrelation: viewer وobject: document:budget، ومعه contextual_tuples فيها user:carol member team:finance، وcontext فيه current_time، وOpenFGA يقرأ أيضاً العلاقات المخزنة من PostgreSQL، والعلاقة السياقية تستخدم لهذا الطلب فقط ولا تحفظ](https://arabroot.io/content/images/2026/10/openfga-practice-02-request-facts.webp)

العلاقة المخزنة دائمة حتى تحذفها، والعلاقة السياقية تعيش طلباً واحداً، وسياق الشرط يحمل قيماً مثل الوقت

```bash
fga query check user:carol viewer document:budget
fga query check user:carol viewer document:budget --contextual-tuple "user:carol member team:finance"
```

```text
{"allowed":false,"resolution":""}
{"allowed":true,"resolution":""}
```

carol ليست في الفريق في المحرك، ولكن عندما قالت الخدمة إن التوكن يذكر أنها في فريق المالية قرأت المستند. وهذا مفيد، وهو أيضاً خطير بالقدر نفسه، والسبب أن العلاقة السياقية تقبل دون أي فحص، فإذا أخذتها الخدمة من طلب المستخدم نفسه، مثل حقل في JSON أو ترويسة يرسلها المتصفح، فإن أي مستخدم يستطيع أن يكتب لنفسه أي عضوية. لذلك لا تبن العلاقة السياقية إلا من توكن تحققت من توقيعه، أو من بيانات خدمتك، وتذكر أن قرار المحرك حينها لا يكون أحدث من التوكن، فإذا أخرج موظف من المجموعة بقيت عضويته في التوكن حتى ينتهي.

## الأسئلة العكسية: ListObjects وListUsers

check يسأل عن مستخدم ومورد، أما في الواجهات فتحتاج غالباً إلى السؤالين العكسيين: ما المستندات التي يراها bob؟ ومن يستطيع أن يرى مستند الرواتب؟

```bash
fga query list-objects user:bob viewer document
fga query list-objects user:khalid viewer document --context '{"current_time":"2026-10-12T12:30:00Z"}'
fga query list-users --object document:budget --relation viewer --user-filter user --context '{"current_time":"2026-10-12T12:30:00Z"}'
fga query list-users --object document:salaries --relation viewer --user-filter user --context '{"current_time":"2026-10-12T13:30:00Z"}'
```

```text
{"objects":["document:budget"]}
{"objects":["document:budget","document:salaries"]}
{"users":[{"object":{"id":"bob","type":"user"}},{"object":{"id":"alice","type":"user"}},{"object":{"id":"khalid","type":"user"}}]}
{"users":[{"object":{"id":"alice","type":"user"}}]}
```

الإجابة الأخيرة هي ما يسأله المدقق: من يستطيع رؤية الرواتب الساعة 13:30؟ alice وحدها، فbob محجوب وkhalid انتهت ساعته، وnoura لا تقرأ. وللسؤالين حدود افتراضية، ألف نتيجة وثلاث ثوان، وقد رأينا في [المنصة](https://arabroot.io/articles/%D9%85%D9%86%D8%B5%D8%A9-%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-microservices/#lists) كيف قطعت قائمة alice عند ألف مستند دون خطأ، لذلك اقرأ [توثيق الأسئلة](https://openfga.dev/docs/interacting/relationship-queries?ref=arabroot.io) قبل أن تبني عليها صفحة. وأما مستوى الاتساق لكل سؤال، `MINIMIZE_LATENCY` أو `HIGHER_CONSISTENCY`، فقد قسنا أثره في [المنصة](https://arabroot.io/articles/%D9%85%D9%86%D8%B5%D8%A9-%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-microservices/#consistency).

## اختبار النموذج في CI

النموذج أعلاه فيه قرارات تصميم كثيرة، وكل واحد منها يمكن أن يكسره تعديل صغير، لذلك نكتب ما نتوقعه في ملف اختبار `store.fga.yaml` يقرأ النموذج والعلاقات نفسها، كما في [توثيق الاختبار](https://openfga.dev/docs/modeling/testing?ref=arabroot.io):

```yaml
name: docs
model_file: ./model.fga
tuple_file: ./tuples.yaml
tests:
  - name: the finance team reads the folder and everything under it
    check:
      - user: user:bob
        object: document:budget
        assertions: { viewer: true, editor: false, can_delete: false }
      - user: user:alice
        object: document:budget
        assertions: { editor: true, can_delete: true }
  - name: a blocked user loses access even through the team
    check:
      - user: user:bob
        object: document:salaries
        assertions: { viewer: false }
  - name: the org admin can delete, but cannot read
    check:
      - user: user:noura
        object: document:budget
        assertions: { can_delete: true, viewer: false }
  - name: the auditor's access ends after one hour
    check:
      - user: user:khalid
        object: document:budget
        context: { current_time: "2026-10-12T12:59:00Z" }
        assertions: { viewer: true }
      - user: user:khalid
        object: document:budget
        context: { current_time: "2026-10-12T13:01:00Z" }
        assertions: { viewer: false }
  - name: a new team member reads the budget (tuples added for this test only)
    tuples:
      - { user: user:carol, relation: member, object: team:finance }
    check:
      - user: user:carol
        object: document:budget
        assertions: { viewer: true, editor: false }
  - name: who can read the salaries document
    list_users:
      - object: document:salaries
        user_filter: [{ type: user }]
        context: { current_time: "2026-10-12T12:30:00Z" }
        assertions:
          viewer:
            users: [user:alice, user:khalid]
    list_objects:
      - user: user:bob
        type: document
        assertions:
          viewer: [document:budget]
```

```bash
fga model test --tests store.fga.yaml
```

```text
# Test Summary #
Tests 6/6 passing
Checks 12/12 passing
ListObjects 1/1 passing
ListUsers 1/1 passing
```

في الملف أعلاه لاحظ أن الاختبار يمكن أن يضيف علاقات لنفسه فقط بالحقل `tuples`، مثل إضافة carol إلى الفريق، وأن الاختبارات تختبر ما لا يجب أن يحدث أيضاً، مثل أن مديرة المؤسسة لا تقرأ. وقد حاولنا في البداية أن نكتب `contextual_tuples` داخل الفحص، فرفضت الأداة الملف برسالة `field contextual_tuples not found in type storetest.ModelTestCheck`، فهذا الحقل غير موجود في صيغة ملف الاختبار في الإصدار 0.8.1.

### نكسرها: نحذف الاستثناء من السطر

لنفرض أن مطوراً رأى السطر طويلاً فبسطه وحذف `but not blocked`:

```diff
-    define viewer: ([user, user with not_expired] or editor or viewer from folder) but not blocked
+    define viewer: [user, user with not_expired] or editor or viewer from folder
```

```text
(FAILING) a blocked user loses access even through the team: Checks (0/1 passing)
ⅹ Check(user=user:bob,relation=viewer,object=document:salaries): expected=false, got=true
---
(FAILING) who can read the salaries document: ListObjects (0/1 passing) | ListUsers (0/1 passing)
ⅹ ListObjects(user=user:bob,relation=viewer,type=document): expected=[document:budget], got=[document:salaries document:budget]
ⅹ ListUsers(object={Type:document Id:salaries},relation=viewer,user_filter={Type:user Relation:<nil>}, context:&map[current_time:2026-10-12T12:30:00Z]): expected={Users:[user:alice user:khalid]}, got={Users:[user:khalid user:bob user:alice]}
---
# Test Summary #
Tests 4/6 passing
Checks 11/12 passing
ListObjects 0/1 passing
ListUsers 0/1 passing
```

ثلاثة اختبارات فشلت، وكلها تقول الشيء نفسه بثلاث طرق: bob أصبح يقرأ الرواتب. وهذا الاختبار لا يحتاج إلى سيرفر، فيعمل في خط CI عند كل طلب دمج، وقد شرحنا بناء هذا الخط في [خط CI/CD آمن](https://arabroot.io/articles/%D8%AE%D8%B7-ci-cd-%D8%A7%D9%85%D9%86-%D9%85%D9%86-%D8%A7%D9%84%D8%A8%D8%AF%D8%A7%D9%8A%D8%A9-%D9%84%D9%84%D9%86%D9%87%D8%A7%D9%8A%D8%A9/).

## تغيير النموذج: كل تعديل نموذج جديد

النماذج في OpenFGA لا تعدل، فكل كتابة لنموذج تنشئ نموذجاً جديداً بمعرف جديد، والقديم يبقى، وكل طلب إما أن يحدد معرف النموذج الذي يريده، وإما أن يستخدم أحدث نموذج. والصورة التالية تبين ما حدث في التجربة عبر أربعة نماذج:

![خط زمني لأربعة نماذج: v1 مع blocked وbut not، وv2 يضيف commenter، وv3 بالأحمر يحذف blocked، وv4 يعيد v2. وتحتها لوحتان: العميل المثبت على v1 يسأل عن commenter فيحصل على relation document#commenter not found ولا يتغير شيء حتى تنقل التثبيت، والعميل الذي يستخدم أحدث نموذج بعد v3 يحصل على bob viewer salaries true لأن علاقة الحجب ما زالت في الجدول ولكن النموذج لم يعد يعرفها](https://arabroot.io/content/images/2026/10/openfga-practice-03-model-versions.webp)

ثبت معرف النموذج في كل خدمة، ولا تنقله إلا بعد أن تمر الاختبارات على النموذج الجديد مع علاقاتك الحقيقية

أولاً تعديل آمن يضيف علاقة جديدة `commenter`:

```bash
unset FGA_MODEL_ID   # like a service that does not pin the model: it uses the latest one
sed 's/    define can_delete: owner or can_delete from folder/    define commenter: [user] or editor\n    define can_delete: owner or can_delete from folder/' model.fga > model-v2.fga
fga model write --file model-v2.fga
fga model list
FGA_MODEL_ID=01M4R1RT71B0915W7XK6HAD76T fga query check user:alice commenter document:budget
fga query check user:alice commenter document:budget
```

```text
{"authorization_model_id":"01M4R1VKGK7KER19MNWSQN4YDC"}
{"authorization_models":[{"id":"01M4R1VKGK7KER19MNWSQN4YDC","created_at":"2026-10-12T12:37:02.099Z"},{"id":"01M4R1RT71B0915W7XK6HAD76T","created_at":"2026-10-12T12:35:30.657Z"}]}
Error: check failed: POST validation error for Check POST with body {"code":"validation_error","message":"invalid relation: relation 'document#commenter' not found"}
{"allowed":true,"resolution":""}
```

الخدمة المثبتة على النموذج الأول لا تعرف العلاقة الجديدة، والخدمة التي لا تحدد نموذجاً استخدمت الجديد مباشرة. والآن تعديل يبدو تنظيفاً: فريق قرر أن الحجب لم يعد مطلوباً فحذف العلاقة `blocked` من النموذج:

```bash
sed -e '/define blocked: \[user\]/d' \
    -e 's/(\[user, user with not_expired\] or editor or viewer from folder) but not blocked/[user, user with not_expired] or editor or viewer from folder/' \
    model-v2.fga > model-v3.fga
fga model write --file model-v3.fga
fga query check user:bob viewer document:salaries
fga tuple read --object document:salaries --relation blocked
fga tuple write user:carol blocked document:salaries
```

```text
{"authorization_model_id":"01M4R1VZQ4NYTEGP09TQ5XQ08G"}
{"allowed":true,"resolution":""}
{
  "continuation_token":"",
  "tuples": [
    {
      "key": {
        "object":"document:salaries",
        "relation":"blocked",
        "user":"user:bob"
      },
      "timestamp":"2026-10-12T12:35:40.96315Z"
    }
  ]
}
Error: failed to write tuple: POST validation error for Write POST with body {"code":"validation_error","message":"Invalid tuple 'document:salaries#blocked@user:carol'. Reason: relation 'document#blocked' not found"}
```

في المخرج أعلاه لاحظ التالي:

- bob أصبح يقرأ الرواتب في اللحظة التي كتب فيها النموذج الجديد، دون أي تغيير في العلاقات، لأن كل خدمة لا تثبت معرف النموذج انتقلت إليه فوراً.
- علاقة الحجب ما زالت في الجدول، والمحرك يتجاهلها لأن النموذج لم يعد يعرف العلاقة، ولن يقبل كتابة علاقة جديدة من هذا النوع.
- لا يوجد «تراجع» عن نموذج، فالرجوع يعني أن تكتب محتوى النموذج القديم من جديد، فيأخذ معرفاً رابعاً، كما في الأمرين التاليين.

```bash
fga model write --file model-v2.fga
fga query check user:bob viewer document:salaries
```

```text
{"authorization_model_id":"01M4R1WCQKJ23ED8SY9PX066DT"}
{"allowed":false,"resolution":""}
```

وعاد bob محجوباً، لأن علاقة الحجب لم تحذف من الجدول أصلاً.

والقواعد التي نخرج بها: كل خدمة تثبت معرف النموذج في إعدادها، ويتغير المعرف بنشر مثل أي إعداد، والنموذج الجديد يمر بالاختبارات قبل الكتابة، وعند حذف علاقة أو إعادة تسميتها تكتب العلاقات بالشكل الجديد أولاً، وتنقل الخدمات، ثم تحذف العلاقات القديمة، وهي الطريقة نفسها التي تغير بها عموداً في قاعدة بيانات تستخدمها عدة خدمات.

## الترقية والنسخ الاحتياطي

كل ما في OpenFGA من حالة دائمة موجود في قاعدة البيانات، لذلك نسخه الاحتياطي هو نسخ قاعدة البيانات كما شرحنا في [استعادة PostgreSQL إلى أي لحظة](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9-postgresql-%D8%A7%D9%84%D9%89-%D8%A7%D9%8A-%D9%84%D8%AD%D8%B8%D8%A9-%D9%85%D8%B9-pgbackrest/)، وقبل أي ترقية خذ نسخة، لأن `openfga migrate` يغير المخطط. والآن نرقي من 1.21.0 إلى 1.22.0:

```bash
docker exec lab-authz-fga-pg pg_dump -U openfga -Fc openfga > openfga-before-upgrade.dump
sed -i 's/^FGA_VERSION=v1.21.0/FGA_VERSION=v1.22.0/' .env; set -a; . ./.env; set +a
docker compose up -d
docker logs lab-authz-fga-migrate 2>&1 | tail -3
fga query check user:bob viewer document:budget
```

```text
INFO	db info	{"current version": 6}
INFO	running all migrations
INFO	migration done
{"allowed":true,"resolution":""}
```

المخطط كان في الإصدار 6، ولم تحتج هذه الترقية إلى تغيير فيه، ولكن الأمر يعمل في كل مرة ولا يضر إذا لم يجد ما يفعله، لذلك اجعله خطوة ثابتة قبل تشغيل أي إصدار جديد. وفي عنقود من عدة نسخ شغل الترحيل مرة واحدة قبل تحديث النسخ، واقرأ [سجل التغييرات](https://github.com/openfga/openfga/blob/main/CHANGELOG.md?ref=arabroot.io) للإصدارات التي تقفز عليها، فالإصدار 1.22.0 مثلاً غير توقيع دالة في حزمة MySQL لمن يضمن OpenFGA في برنامجه بلغة Go.

## OpenFGA من داخل خدمة بلغة Python

الخدمة في الإنتاج تستخدم المكتبة الرسمية بدل أداة سطر الأوامر، وهذه خدمة صغيرة بالمكتبة [openfga-sdk](https://github.com/openfga/python-sdk?ref=arabroot.io) بإصدارها 0.10.5، تحول المجموعات من التوكن إلى علاقات سياقية وترسل الوقت الحالي كسياق للشرط، وتثبت معرف النموذج:

```python
# How a service asks OpenFGA through the official Python SDK (sync client).
# The team membership that comes from the identity provider's token is sent as a contextual tuple:
# it is used for this request only and never stored.
import os, time
from datetime import datetime, timezone
from openfga_sdk.client import ClientConfiguration
from openfga_sdk.client.models import ClientCheckRequest, ClientListObjectsRequest, ClientTuple
from openfga_sdk.credentials import CredentialConfiguration, Credentials
from openfga_sdk.sync import OpenFgaClient

config = ClientConfiguration(
    api_url="http://lab-authz-fga:8080",
    store_id=os.environ["FGA_STORE_ID"],
    authorization_model_id=os.environ["FGA_MODEL_ID"],      # pin the model; never "latest" in production
    credentials=Credentials(method="api_token",
                            configuration=CredentialConfiguration(api_token=os.environ["FGA_KEY"])))

def facts(user, groups):
    """Groups from the token become contextual tuples; the clock becomes the condition context."""
    tuples = [ClientTuple(user=user, relation="member", object=f"team:{g}") for g in groups]
    return tuples, {"current_time": datetime.now(timezone.utc).isoformat()}

with OpenFgaClient(config) as fga:
    for user, groups in (("user:carol", ["finance"]), ("user:carol", []), ("user:khalid", [])):
        tuples, ctx = facts(user, groups)
        r = fga.check(ClientCheckRequest(user=user, relation="viewer", object="document:budget",
                                         contextual_tuples=tuples, context=ctx))
        print(f"check  {user:12} groups={groups!s:12} viewer document:budget -> {r.allowed}")
    tuples, ctx = facts("user:carol", ["finance"])
    r = fga.list_objects(ClientListObjectsRequest(user="user:carol", relation="viewer", type="document",
                                                  contextual_tuples=tuples, context=ctx))
    print("list   user:carol    groups=['finance'] ->", r.objects)
    times = []
    for _ in range(500):
        t0 = time.perf_counter()
        fga.check(ClientCheckRequest(user="user:bob", relation="viewer", object="document:budget"))
        times.append((time.perf_counter() - t0) * 1000)
    times.sort()
    print(f"500 checks through the SDK: p50={times[250]:.2f}ms p95={times[475]:.2f}ms p99={times[495]:.2f}ms")
```

```bash
docker run --rm --name lab-authz-fga-app --network lab-authz-fga-net -v "$PWD:/work" -w /work \
  -e FGA_KEY -e FGA_STORE_ID=01M4R1RT6EJCMAQ3P7KBY8K9R3 -e FGA_MODEL_ID=01M4R1WCQKJ23ED8SY9PX066DT \
  python:3.13.16-slim sh -c "pip install -q openfga-sdk==0.10.5; python app.py"
```

```text
check  user:carol   groups=['finance']  viewer document:budget -> True
check  user:carol   groups=[]           viewer document:budget -> False
check  user:khalid  groups=[]           viewer document:budget -> True
list   user:carol    groups=['finance'] -> ['document:budget', 'document:salaries']
500 checks through the SDK: p50=4.81ms p95=7.66ms p99=10.05ms
```

في المخرج أعلاه لاحظ أن khalid يقرأ لأن التجربة نفذت الساعة 12:38 أي داخل ساعته، وأن carol ترى الرواتب أيضاً لأنها ليست محجوبة، والحجب في نموذجنا خاص ب bob. والمعرف `...6DT` هو النموذج الرابع، أي v2 بعد إعادة كتابته.

## المراقبة: ماذا تقرأ من المقاييس؟

بعد طلبات ال SDK قرأنا المقاييس من المنفذ 18212:

```bash
curl -s 127.0.0.1:18212/metrics | grep -E '^openfga_(datastore_query_count|dispatch_count)_(sum|count)|^grpc_server_handled_total|^openfga_check_cache'
```

```text
grpc_server_handled_total{grpc_code="OK",grpc_method="Check",grpc_service="openfga.v1.OpenFGAService",grpc_type="unary"} 504
grpc_server_handled_total{grpc_code="OK",grpc_method="ListObjects",grpc_service="openfga.v1.OpenFGAService",grpc_type="unary"} 1
openfga_check_cache_hit_count 0
openfga_check_cache_invalid_hit_count 0
openfga_check_cache_total_count 0
openfga_datastore_query_count_sum{grpc_method="check",grpc_service="openfga.v1.OpenFGAService"} 18625
openfga_datastore_query_count_count{grpc_method="check",grpc_service="openfga.v1.OpenFGAService"} 504
openfga_dispatch_count_sum{grpc_method="check",grpc_service="openfga.v1.OpenFGAService"} 2977
openfga_dispatch_count_count{grpc_method="check",grpc_service="openfga.v1.OpenFGAService"} 504
openfga_request_duration_ms_count{consistency="UNSPECIFIED",datastore_query_count="50",dispatch_count="50",grpc_method="check",grpc_service="openfga.v1.OpenFGAService"} 504
```

في المخرج أعلاه لاحظ التالي:

- كل فحص ل bob على الميزانية احتاج في المتوسط إلى 37 استعلاماً في قاعدة البيانات (18625 ÷ 504) ونحو ستة تفرعات Dispatches، لأن النموذج يمشي الفريق والمجلدين والحجب والشرط. وهذا هو الرقم الذي تراقبه عندما يبطؤ المحرك: إذا زاد بعد تعديل النموذج فالتعديل جعل الطريق أطول.
- المقياس `openfga_request_duration_ms` موزع حسب عدد الاستعلامات والتفرعات، فتستطيع أن ترى هل البطء في كل الطلبات أم في الطلبات التي تمشي طرقاً طويلة فقط.
- عدادات `openfga_check_cache` صفر لأن ال Cache غير مفعل هنا، وعندما تفعله راقب نسبة النجاح فيه، فإذا كانت منخفضة فأنت تدفع ثمن القرار القديم دون فائدة.
- وهناك أيضاً `/healthz` للفحص، وقد رد في التجربة ب `{"status":"SERVING"}`.

والسجل بصيغة JSON يكتب لكل طلب الطلب كاملاً، وهذا جزء من سطر حقيقي من التجربة:

```json
{"level":"info","msg":"grpc_req_complete","grpc_method":"Check","user_agent":"openfga-cli/0.8.1","raw_request":{"store_id":"01M4R1RT6EJCMAQ3P7KBY8K9R3","tuple_key":{"user":"user:khalid","relation":"viewer","object":"document:budget"},"contextual_tuples":{"tuple_keys":[]},"authorization_model_id":"01M4R1RT71B0915W7XK6HAD76T","trace":false,"context":{},"consistency":"UNSPECIFIED"}}
```

وهو مفيد في التحقيق، ولكنه يحمل معرفات المستخدمين والسياق، فعامله كسجل فيه بيانات شخصية من حيث الاحتفاظ والصلاحيات، كما شرحنا في [السجلات Logging](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/).

## من غير هذه العلاقة؟ ReadChanges

الجدول `changelog` يحفظ كل كتابة وحذف، وتقرؤه بالأمر `fga tuple changes`، وهو ما تبني عليه مهمة المطابقة في [المنصة](https://arabroot.io/articles/%D9%85%D9%86%D8%B5%D8%A9-%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-microservices/#sync). حذفنا عضوية bob ثم قرأنا تغييرات نوع `team`:

```bash
fga tuple delete user:bob member team:finance
fga tuple changes --type team
fga tuple write user:bob member team:finance
```

```text
TUPLE_OPERATION_WRITE user:bob member team:finance 2026-10-12T12:35:40.95398Z
TUPLE_OPERATION_WRITE user:alice member team:finance 2026-10-12T12:35:40.953732Z
TUPLE_OPERATION_WRITE organization:acme org team:finance 2026-10-12T12:35:40.953063Z
TUPLE_OPERATION_DELETE user:bob member team:finance 2026-10-12T15:02:01.142281Z
```

اختصرنا المخرج إلى سطر لكل تغيير. ولاحظ أن السجل يقول ماذا تغير ومتى، ولا يقول من غيره، لأن المحرك لا يعرف المستخدم الذي ضغط زر المشاركة، فهذا يحفظ في خدمتك مع صف ال Outbox أو في [سجل التدقيق](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D8%AA%D8%AF%D9%82%D9%8A%D9%82-audit-trail-%D9%85%D9%86-%D9%81%D8%B9%D9%84-%D9%85%D8%A7%D8%B0%D8%A7-%D9%88%D9%85%D8%AA%D9%89/).

### إزالة التجربة

```bash
docker compose down -v
rm -rf ~/openfga-lab
```

والأمر يحذف Containers المشروع وشبكته و Volume قاعدة البيانات `lab-authz-fga-pg-data` فقط، ولا يلمس أي شيء آخر على الجهاز.

## الأخطاء الشائعة

- ترك الواجهة دون مصادقة: القيمة الافتراضية `none`، ومن يصل إلى الكتابة يصبح مالكاً لما يريد.
- خدمات لا تثبت معرف النموذج، فيغير نشر نموذج جديد قراراتها في اللحظة نفسها، كما فتح حذف `blocked` الرواتب ل bob.
- صلاحيات إدارية تشمل القراءة تلقائياً لأنها أسهل في الكتابة، مثل `viewer: ... or admin from org`.
- بناء علاقات سياقية من بيانات يرسلها المستخدم بدل التوكن المتحقق منه.
- إرسال سياق افتراضي للشرط حتى يختفي الخطأ، فتصبح الصلاحية المؤقتة دائمة.
- الاعتماد على Playground في الإنتاج، وهي معطلة افتراضياً وستزال.
- الترقية دون نسخة من قاعدة البيانات ودون تشغيل `openfga migrate` أولاً.
- نموذج بلا اختبارات، أو اختبارات تختبر السماح فقط ولا تختبر الرفض.

## الخلاصة

- OpenFGA على PostgreSQL يحفظ العلاقات والنماذج وسجل التغييرات في ستة جداول، وواجهته مفتوحة افتراضياً، فاجعل لها مصادقة بمفتاح أو OIDC.
- النموذج يعبر عن الفرق كمجموعات مستخدمين، والمجلدات المتداخلة بالوراثة `from parent`، والاستثناء ب `but not`، والصلاحية المؤقتة بشرط CEL يأخذ الوقت من الطلب، والسياق الناقص خطأ يعامل كرفض.
- العلاقات السياقية تجلب حقائق من التوكن دون مزامنة، وتقبل دون فحص، لذلك لا تبنها إلا من توكن متحقق منه.
- ListObjects وListUsers يجيبان عن «ماذا يرى» و«من يرى»، واختبارات `fga model test` كشفت حذف الاستثناء بثلاث طرق.
- كل نموذج جديد له معرف جديد، والخدمة التي لا تثبت المعرف تتغير قراراتها فوراً، وحذف علاقة واحدة من النموذج فتح الرواتب ل bob مع بقاء علاقة الحجب في الجدول.
- الترقية خطوة `migrate` بعد نسخة احتياطية، والمقاييس تخبرك بعدد استعلامات كل فحص، وهي أول ما تنظر إليه عندما يبطؤ المحرك.

[![](https://arabroot.io/content/images/2026/10/authorization-cover-cover.webp)دليلAuthorization: كيف تجمع قرارات الصلاحيات في مكان واحدلماذا تتحول شروط الصلاحيات المتفرقة في الكود إلى ثغرات، وكيف تنقل القرار إلى محرك سياسات واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/)[![](https://arabroot.io/content/images/2026/10/authz-platform-cover-cover.webp)دليلمنصة صلاحيات مركزية لخدماتك Microservicesكيف تجعل كل خدماتك تسأل نقطة قرار واحدة، وتبقي علاقاتها متزامنة مع بياناتك، وماذا يحدث عندما يتأخر سحب الصلاحية.](https://arabroot.io/articles/%D9%85%D9%86%D8%B5%D8%A9-%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-microservices/)[![](https://arabroot.io/content/images/2026/10/opa-practice-cover-cover.webp)دليلOPA عملياً: سياسات ABAC لخدماتك وبواباتككيف تكتب سياسات ABAC بلغة Rego وتختبرها، ثم توزعها موقعة على خدماتك وبواباتك وتسجل كل قرار تتخذه.](https://arabroot.io/articles/opa-%D8%B9%D9%85%D9%84%D9%8A%D8%A7-%D8%B3%D9%8A%D8%A7%D8%B3%D8%A7%D8%AA-abac-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-%D9%88%D8%A8%D9%88%D8%A7%D8%A8%D8%A7%D8%AA%D9%83/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على OpenFGA 1.21.0 ثم 1.22.0 مع fga CLI 0.8.1 وopenfga-sdk 0.10.5 وPostgreSQL 18.6 على Docker Engine 29.8.0.