Skip to content

التفويض

يُنفّذ FORGE نظام تحكم بالوصول كامل مبني على الأدوار (RBAC) يحكم ما يمكن للمستخدمين المُصادق عليهم فعله داخل التطبيق. الأدوار تجمع الصلاحيات معاً، والصلاحيات تتحكم بالوصول للإجراءات الفردية على الموارد. النظام مرن بما يكفي لدعم إعدادات admin/user البسيطة والتسلسلات الهرمية المعقدة متعددة الأدوار.

معمارية RBAC

┌──────────┐       ┌──────────┐       ┌──────────────┐
│ المستخدمون│──M:N──│  الأدوار │──M:N──│  الصلاحيات   │
└──────────┘       └──────────┘       └──────────────┘
                   role_user           permission_role

المستخدم "أحمد" ──▶ الدور "admin" ──▶ الصلاحية "users.create"
                                   ──▶ الصلاحية "users.edit"
                                   ──▶ الصلاحية "roles.view"
                                   ──▶ ...جميع الصلاحيات

يمكن للمستخدم أن يكون له أدوار متعددة، وكل دور يمكن أن يكون له صلاحيات متعددة. عند التحقق من التفويض، يُجمّع النظام جميع الصلاحيات من جميع أدوار المستخدم.

مخطط قاعدة البيانات

أربعة جداول تشغّل نظام RBAC:

sql
-- Roles table
CREATE TABLE roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL UNIQUE,
    display_name VARCHAR(255),
    description TEXT,
    is_system BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Permissions table
CREATE TABLE permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL UNIQUE,
    display_name VARCHAR(255),
    description TEXT,
    group_name VARCHAR(255),
    is_system BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Junction table: which users have which roles
CREATE TABLE role_user (
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id)
);

-- Junction table: which roles have which permissions
CREATE TABLE permission_role (
    permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (permission_id, role_id)
);

تحذير

الصفوف التي فيها is_system = true لا يمكن حذفها أو إعادة تسميتها عبر API. هذا يحمي دور admin المدمج والصلاحيات الأساسية من الإزالة العرضية.

الأدوار الافتراضية

يبذر FORGE دورين عند أول ترحيل:

الدورالصلاحياتنظاميالوصف
adminجميع الصلاحياتنعموصول كامل لكل مورد
userبدون صلاحياتنعممُصادق عليه لكن بدون امتيازات

دور admin يتلقى تلقائياً كل صلاحية. عند إضافة صلاحيات جديدة عبر الـ seeders، يُحدّث دور admin ليشملها.

اصطلاح تسمية الصلاحيات

الصلاحيات تتبع صيغة resource.action:

users.view        # View user list and details
users.create      # Create new users
users.edit        # Update existing users
users.delete      # Delete users

roles.view        # View roles
roles.create      # Create roles
roles.edit        # Update roles
roles.delete      # Delete roles

contents.view     # View content entries
contents.create   # Create content
contents.edit     # Update content
contents.delete   # Delete content

settings.view     # View application settings
settings.edit     # Update settings

نصيحة

عند إضافة وحدة جديدة، اتبع الاصطلاح وأنشئ أربع صلاحيات: module.view و module.create و module.edit و module.delete. دور admin سيلتقطها تلقائياً من الـ seeder.

استخدام Middleware

حماية المسارات بالصلاحيات

طبّق middleware الـ require_permission على المسارات التي تحتاج تفويضاً:

rust
use crate::middleware::rbac::require_permission;

pub fn routes(pool: PgPool) -> Router {
    Router::new()
        // Only users with "users.view" permission can list users
        .route(
            "/users",
            get(handlers::admin::users::list)
                .route_layer(middleware::from_fn_with_state(
                    pool.clone(),
                    require_permission("users.view"),
                )),
        )
        // Only users with "users.create" permission can create users
        .route(
            "/users",
            post(handlers::admin::users::create)
                .route_layer(middleware::from_fn_with_state(
                    pool.clone(),
                    require_permission("users.create"),
                )),
        )
}

التحقق من الصلاحيات في الـ Handlers

للتحكم الدقيق داخل handler، تحقق من الصلاحيات مباشرة على المستخدم المُصادق عليه:

rust
pub async fn update_user(
    State(pool): State<PgPool>,
    Extension(current_user): Extension<AuthUser>,
    Path(user_id): Path<Uuid>,
    Json(payload): Json<UpdateUserRequest>,
) -> Result<impl IntoResponse, AppError> {
    // Check if user can edit other users
    if !current_user.has_permission("users.edit") {
        return Err(AppError::Forbidden(
            "You do not have permission to edit users".into(),
        ));
    }

    // Additional check: prevent non-admins from assigning admin role
    if payload.role_ids.contains(&admin_role_id)
        && !current_user.has_permission("roles.edit")
    {
        return Err(AppError::Forbidden(
            "You do not have permission to assign admin role".into(),
        ));
    }

    let user = UserService::update(&pool, user_id, payload).await?;
    Ok(Json(ApiResponse::success(user)))
}

بنية AuthUser

middleware المصادقة يحقن AuthUser في request extensions. هذه البنية تحمل هوية المستخدم وصلاحياته المُجمّعة:

rust
#[derive(Clone, Debug)]
pub struct AuthUser {
    pub id: Uuid,
    pub email: String,
    pub name: String,
    pub permissions: Vec<String>,
    pub roles: Vec<String>,
}

impl AuthUser {
    /// Check if user has a specific permission
    pub fn has_permission(&self, permission: &str) -> bool {
        self.permissions.contains(&permission.to_string())
    }

    /// Check if user has any of the given permissions
    pub fn has_any_permission(&self, permissions: &[&str]) -> bool {
        permissions.iter().any(|p| self.has_permission(p))
    }

    /// Check if user has all of the given permissions
    pub fn has_all_permissions(&self, permissions: &[&str]) -> bool {
        permissions.iter().all(|p| self.has_permission(p))
    }

    /// Check if user has a specific role
    pub fn has_role(&self, role: &str) -> bool {
        self.roles.contains(&role.to_string())
    }
}

إدارة الأدوار عبر API

تعيين أدوار لمستخدم

bash
POST /api/admin/users/{id}/roles
Authorization: Bearer {token}
Content-Type: application/json

{
  "role_ids": [
    "550e8400-e29b-41d4-a716-446655440000",
    "660e8400-e29b-41d4-a716-446655440001"
  ]
}

عرض جميع الأدوار

bash
GET /api/admin/roles
Authorization: Bearer {token}

إنشاء دور مخصص

bash
POST /api/admin/roles
Authorization: Bearer {token}
Content-Type: application/json

{
  "name": "editor",
  "display_name": "محرر",
  "description": "يمكنه إدارة المحتوى لكن ليس المستخدمين",
  "permission_ids": [
    "uuid-of-contents.view",
    "uuid-of-contents.create",
    "uuid-of-contents.edit"
  ]
}

تحديث صلاحيات الدور

bash
PUT /api/admin/roles/{id}
Authorization: Bearer {token}
Content-Type: application/json

{
  "display_name": "محرر أول",
  "permission_ids": [
    "uuid-of-contents.view",
    "uuid-of-contents.create",
    "uuid-of-contents.edit",
    "uuid-of-contents.delete"
  ]
}

تحذير

الأدوار النظامية (is_system = true) لا يمكن حذفها عبر API. محاولة حذف دور admin أو user تُرجع خطأ 403 Forbidden. لا يزال بإمكانك تعديل الصلاحيات المُعيّنة للأدوار النظامية.

Permission Seeder

الصلاحيات تُبذر من ملف ترحيل لضمان الاتساق عبر البيئات:

sql
-- database/seeders/02_permissions.sql
INSERT INTO permissions (name, display_name, group_name, is_system)
VALUES
    ('users.view', 'View Users', 'Users', true),
    ('users.create', 'Create Users', 'Users', true),
    ('users.edit', 'Edit Users', 'Users', true),
    ('users.delete', 'Delete Users', 'Users', true),
    ('roles.view', 'View Roles', 'Roles', true),
    ('roles.create', 'Create Roles', 'Roles', true),
    ('roles.edit', 'Edit Roles', 'Roles', true),
    ('roles.delete', 'Delete Roles', 'Roles', true),
    ('contents.view', 'View Content', 'Content', true),
    ('contents.create', 'Create Content', 'Content', true),
    ('contents.edit', 'Edit Content', 'Content', true),
    ('contents.delete', 'Delete Content', 'Content', true)
ON CONFLICT (name) DO NOTHING;

-- Assign all permissions to admin role
INSERT INTO permission_role (permission_id, role_id)
SELECT p.id, r.id
FROM permissions p
CROSS JOIN roles r
WHERE r.name = 'admin'
ON CONFLICT DO NOTHING;

انظر أيضاً

Released under the MIT License.