Enterprise-grade Role-Based Access Control (RBAC) library for PHP

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/feruzlabs/rbac

v1.0.0 2025-08-10 09:47 UTC

This package is auto-updated.

Last update: 2025-09-10 09:58:08 UTC


README

Enterprise-grade Role-Based Access Control (RBAC) library for PHP applications.

๐Ÿš€ Features

  • Clean Architecture + Domain-Driven Design (DDD) - Professional code structure
  • Framework-agnostic - Works with any PHP framework or vanilla PHP
  • Multi-tenant support - Organizations for SaaS applications
  • Role hierarchy - Inherit permissions from parent roles
  • Group-based assignments - Assign roles to groups, users inherit group roles
  • Fine-grained permissions - Resource-action model for precise control
  • Built-in and custom roles - System roles and user-defined roles
  • Immutable entities - Thread-safe and predictable behavior
  • UUIDv7 support - Time-ordered UUIDs for better database performance
  • Comprehensive testing - 100% test coverage with PHPUnit

๐Ÿ“‹ Requirements

  • PHP 8.3 or higher
  • Composer

๐Ÿ›  Installation

Install via Composer:

composer require feruzlabs/rbac

๐Ÿ“š Quick Start

Basic Usage

<?php

use Feruzlabs\Rbac\Domain\Entity\User;
use Feruzlabs\Rbac\Domain\Entity\Role;
use Feruzlabs\Rbac\Domain\Entity\Permission;
use Feruzlabs\Rbac\Domain\Entity\Resource;
use Feruzlabs\Rbac\Domain\Entity\Action;
use Feruzlabs\Rbac\Application\UseCase\AssignRoleToUser;
use Feruzlabs\Rbac\Application\UseCase\CheckUserPermission;
use Feruzlabs\Rbac\Application\DTO\AssignRoleToUserRequest;
use Feruzlabs\Rbac\Application\DTO\CheckUserPermissionRequest;

// 1. Create entities
$user = User::create(
    username: 'john_doe',
    email: 'john@example.com',
    passwordHash: password_hash('password123', PASSWORD_DEFAULT),
    fullName: 'John Doe'
);

$resource = Resource::create(
    name: 'article',
    type: 'content'
);

$action = Action::create(
    name: 'read',
    description: 'Read articles'
);

$permission = Permission::create(
    resourceId: $resource->id,
    actionId: $action->id,
    name: 'article:read',
    description: 'Permission to read articles'
);

$role = Role::create(
    name: 'editor',
    description: 'Content editor role'
);

// 2. Save entities (using your repository implementations)
$userRepository->save($user);
$resourceRepository->save($resource);
$actionRepository->save($action);
$permissionRepository->save($permission);
$roleRepository->save($role);

// 3. Assign role to user
$assignRoleUseCase = new AssignRoleToUser($rbacService);
$assignRoleUseCase->execute(new AssignRoleToUserRequest(
    userId: $user->id->toString(),
    roleId: $role->id->toString()
));

// 4. Check permissions
$checkPermissionUseCase = new CheckUserPermission($permissionChecker);
$result = $checkPermissionUseCase->execute(new CheckUserPermissionRequest(
    userId: $user->id->toString(),
    permissionId: $permission->id->toString()
));

if ($result->hasPermission) {
    echo "User has permission!";
} else {
    echo "Permission denied: " . $result->reason;
}

๐Ÿข Multi-tenant Usage

<?php

use Feruzlabs\Rbac\Domain\Entity\Organization;

// Create organizations
$acmeCorp = Organization::create('ACME Corporation');
$techStartup = Organization::create('Tech Startup');

// Create organization-specific roles
$acmeManager = Role::create(
    name: 'manager',
    organizationId: $acmeCorp->id,
    description: 'ACME manager role'
);

$techAdmin = Role::create(
    name: 'admin',
    organizationId: $techStartup->id,
    description: 'Tech startup admin role'
);

// Users can have different roles in different organizations
$user = User::create('john_doe', 'john@example.com', 'hash');

// Assign role in ACME
$assignRoleUseCase->execute(new AssignRoleToUserRequest(
    userId: $user->id->toString(),
    roleId: $acmeManager->id->toString()
));

// Check permissions in specific organization context
$hasPermission = $permissionChecker->userHasPermission(
    $user->id,
    $permission->id
);

๐Ÿ‘ฅ Group-based Role Assignment

<?php

use Feruzlabs\Rbac\Domain\Entity\Group;

// Create groups
$developers = Group::create(
    organizationId: $organization->id,
    name: 'developers',
    description: 'Development team'
);

$qaTeam = Group::create(
    organizationId: $organization->id,
    name: 'qa_team',
    description: 'Quality Assurance team'
);

// Assign roles to groups
$rbacService->assignRoleToGroup($developers->id, $developerRole->id);
$rbacService->assignRoleToGroup($qaTeam->id, $qaRole->id);

// Add users to groups
$userGroupService->addUserToGroup($user->id, $developers->id);

// User now has all roles from the developers group
$userRoles = $rbacService->getUserRoles($user->id);

๐Ÿ”— Framework Integration

Laravel Integration

1. Service Provider

<?php
// app/Providers/RbacServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Feruzlabs\Rbac\Infrastructure\Persistence\Eloquent\EloquentUserRepository;
use Feruzlabs\Rbac\Infrastructure\Persistence\Eloquent\EloquentRoleRepository;
use Feruzlabs\Rbac\Infrastructure\Persistence\Eloquent\EloquentPermissionRepository;
use Feruzlabs\Rbac\Infrastructure\Security\LaravelPermissionChecker;
use Feruzlabs\Rbac\Domain\Service\RbacService;

class RbacServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(EloquentUserRepository::class);
        $this->app->singleton(EloquentRoleRepository::class);
        $this->app->singleton(EloquentPermissionRepository::class);
        $this->app->singleton(LaravelPermissionChecker::class);
        $this->app->singleton(RbacService::class);
    }
}

2. Eloquent Repository Implementation

<?php
// app/Repositories/EloquentUserRepository.php

namespace App\Repositories;

use Feruzlabs\Rbac\Domain\Repository\UserRepositoryInterface;
use Feruzlabs\Rbac\Domain\Entity\User;
use Feruzlabs\Rbac\Domain\ValueObject\UserId;
use App\Models\User as UserModel;

class EloquentUserRepository implements UserRepositoryInterface
{
    public function save(User $user): void
    {
        UserModel::updateOrCreate(
            ['id' => $user->id->toString()],
            [
                'username' => $user->username,
                'email' => $user->email,
                'password_hash' => $user->passwordHash,
                'full_name' => $user->fullName,
                'is_active' => $user->isActive,
                'created_at' => $user->createdAt,
                'updated_at' => $user->updatedAt,
            ]
        );
    }

    public function findById(UserId $id): ?User
    {
        $model = UserModel::find($id->toString());
        
        if (!$model) {
            return null;
        }

        return new User(
            id: UserId::fromString($model->id),
            username: $model->username,
            email: $model->email,
            passwordHash: $model->password_hash,
            fullName: $model->full_name,
            isActive: $model->is_active,
            createdAt: $model->created_at,
            updatedAt: $model->updated_at
        );
    }

    // ... implement other methods
}

3. Middleware

<?php
// app/Http/Middleware/CheckPermission.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Feruzlabs\Rbac\Application\UseCase\CheckUserPermission;
use Feruzlabs\Rbac\Application\DTO\CheckUserPermissionRequest;

class CheckPermission
{
    public function __construct(
        private CheckUserPermission $checkPermissionUseCase
    ) {}

    public function handle(Request $request, Closure $next, string $permission)
    {
        $user = $request->user();
        
        if (!$user) {
            abort(401);
        }

        $result = $this->checkPermissionUseCase->execute(
            new CheckUserPermissionRequest(
                userId: $user->id,
                permissionName: $permission
            )
        );

        if (!$result->hasPermission) {
            abort(403, $result->reason);
        }

        return $next($request);
    }
}

4. Blade Directives

<?php
// app/Providers/RbacServiceProvider.php

use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    Blade::if('can', function ($permission) {
        $user = auth()->user();
        if (!$user) return false;
        
        return app(CheckUserPermission::class)->execute(
            new CheckUserPermissionRequest(
                userId: $user->id,
                permissionName: $permission
            )
        )->hasPermission;
    });
}

5. Usage in Routes

// routes/web.php

Route::middleware(['auth', 'permission:article:read'])->group(function () {
    Route::get('/articles', [ArticleController::class, 'index']);
});

Route::middleware(['auth', 'permission:article:write'])->group(function () {
    Route::post('/articles', [ArticleController::class, 'store']);
});

6. Usage in Blade Templates

@can('article:read')
    <div class="articles">
        @foreach($articles as $article)
            <div class="article">{{ $article->title }}</div>
        @endforeach
    </div>
@endcan

@can('article:write')
    <a href="/articles/create" class="btn">Create Article</a>
@endcan

Symfony Integration

1. Service Configuration

# config/services.yaml

services:
    Feruzlabs\Rbac\Infrastructure\Persistence\Doctrine\DoctrineUserRepository:
        arguments:
            $entityManager: '@doctrine.orm.entity_manager'
    
    Feruzlabs\Rbac\Infrastructure\Persistence\Doctrine\DoctrineRoleRepository:
        arguments:
            $entityManager: '@doctrine.orm.entity_manager'
    
    Feruzlabs\Rbac\Infrastructure\Security\SymfonyPermissionChecker:
        arguments:
            $permissionRepository: '@Feruzlabs\Rbac\Infrastructure\Persistence\Doctrine\DoctrinePermissionRepository'
    
    Feruzlabs\Rbac\Domain\Service\RbacService:
        arguments:
            $userRepository: '@Feruzlabs\Rbac\Infrastructure\Persistence\Doctrine\DoctrineUserRepository'
            $roleRepository: '@Feruzlabs\Rbac\Infrastructure\Persistence\Doctrine\DoctrineRoleRepository'
            $permissionRepository: '@Feruzlabs\Rbac\Infrastructure\Persistence\Doctrine\DoctrinePermissionRepository'

2. Security Voter

<?php
// src/Security/Voter/RbacVoter.php

namespace App\Security\Voter;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Feruzlabs\Rbac\Application\UseCase\CheckUserPermission;
use Feruzlabs\Rbac\Application\DTO\CheckUserPermissionRequest;

class RbacVoter extends Voter
{
    public function __construct(
        private CheckUserPermission $checkPermissionUseCase
    ) {}

    protected function supports(string $attribute, mixed $subject): bool
    {
        return str_contains($attribute, ':');
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        
        if (!$user) {
            return false;
        }

        $result = $this->checkPermissionUseCase->execute(
            new CheckUserPermissionRequest(
                userId: $user->getId(),
                permissionName: $attribute
            )
        );

        return $result->hasPermission;
    }
}

3. Controller Usage

<?php
// src/Controller/ArticleController.php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class ArticleController extends AbstractController
{
    #[Route('/articles', name: 'article_index')]
    #[IsGranted('article:read')]
    public function index(): Response
    {
        // Only users with article:read permission can access this
        return $this->render('article/index.html.twig');
    }

    #[Route('/articles/new', name: 'article_new')]
    #[IsGranted('article:write')]
    public function new(): Response
    {
        // Only users with article:write permission can access this
        return $this->render('article/new.html.twig');
    }
}

Vanilla PHP Integration

1. Simple Implementation

<?php
// config/rbac.php

use Feruzlabs\Rbac\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use Feruzlabs\Rbac\Infrastructure\Persistence\InMemory\InMemoryRoleRepository;
use Feruzlabs\Rbac\Infrastructure\Persistence\InMemory\InMemoryPermissionRepository;
use Feruzlabs\Rbac\Infrastructure\Security\InMemoryPermissionChecker;
use Feruzlabs\Rbac\Domain\Service\RbacService;

// Initialize repositories
$userRepository = new InMemoryUserRepository();
$roleRepository = new InMemoryRoleRepository();
$permissionRepository = new InMemoryPermissionRepository($roleRepository);

// Initialize services
$permissionChecker = new InMemoryPermissionChecker($permissionRepository);
$rbacService = new RbacService(
    $userRepository,
    $roleRepository,
    $permissionRepository,
    new InMemoryUserRoleAssignmentService($roleRepository),
    new InMemoryGroupRoleAssignmentService($roleRepository)
);

// Use cases
$assignRoleUseCase = new AssignRoleToUser($rbacService);
$checkPermissionUseCase = new CheckUserPermission($permissionChecker);

2. Simple Permission Check Function

<?php
// helpers/permission.php

function can(string $permission): bool
{
    global $checkPermissionUseCase;
    
    $user = getCurrentUser(); // Your user retrieval logic
    
    if (!$user) {
        return false;
    }

    $result = $checkPermissionUseCase->execute(
        new CheckUserPermissionRequest(
            userId: $user->id,
            permissionName: $permission
        )
    );

    return $result->hasPermission;
}

// Usage
if (can('article:read')) {
    echo "User can read articles";
}

๐Ÿงช Testing

Running Tests

# Run all tests
vendor/bin/phpunit

# Run with coverage report
vendor/bin/phpunit --coverage-html coverage

# Run specific test suite
vendor/bin/phpunit --testsuite Unit

Writing Tests

<?php
// tests/Unit/YourTest.php

use Feruzlabs\Rbac\Tests\Unit\TestCase;
use Feruzlabs\Rbac\Domain\Entity\User;
use Feruzlabs\Rbac\Domain\Entity\Role;

class YourTest extends TestCase
{
    public function testUserCanHaveRole(): void
    {
        $user = User::create('test_user', 'test@example.com', 'hash');
        $role = Role::create('test_role', 'Test role');
        
        $this->assertInstanceOf(User::class, $user);
        $this->assertInstanceOf(Role::class, $role);
    }
}

๐Ÿ“Š Database Schema

The library is designed to work with the following PostgreSQL schema:

-- Core tables
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    username VARCHAR(100) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(200),
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP
);

CREATE TABLE organizations (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(200) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP
);

CREATE TABLE roles (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    org_id UUID REFERENCES organizations(id),
    name VARCHAR(150) NOT NULL,
    description TEXT,
    is_builtin BOOLEAN DEFAULT false,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP,
    UNIQUE(org_id, name)
);

CREATE TABLE permissions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    resource_id UUID NOT NULL,
    action_id UUID NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(resource_id, action_id)
);

-- Junction tables
CREATE TABLE user_roles (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) NOT NULL,
    role_id UUID REFERENCES roles(id) NOT NULL,
    assigned_by UUID REFERENCES users(id),
    assigned_at TIMESTAMP DEFAULT NOW(),
    expires_at TIMESTAMP,
    UNIQUE(user_id, role_id)
);

CREATE TABLE role_permissions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    role_id UUID REFERENCES roles(id) NOT NULL,
    permission_id UUID REFERENCES permissions(id) NOT NULL,
    allow BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(role_id, permission_id)
);

๐Ÿ”ง Advanced Usage

Role Hierarchy

<?php

// Create parent and child roles
$parentRole = Role::create('manager', 'Manager role');
$childRole = Role::create('supervisor', 'Supervisor role');

// Set up hierarchy (supervisor inherits from manager)
$roleHierarchyService->addChildRole($parentRole->id, $childRole->id);

// User with supervisor role now has manager permissions too
$user = User::create('john', 'john@example.com', 'hash');
$rbacService->assignRoleToUser($user->id, $childRole->id);

// Check inherited permissions
$hasManagerPermission = $permissionChecker->userHasPermission(
    $user->id,
    $managerPermission->id
); // Returns true due to inheritance

Permission Constraints

<?php

// Create permission with constraint
$permission = Permission::create(
    resourceId: $articleResource->id,
    actionId: $editAction->id,
    name: 'article:edit_own',
    description: 'Edit own articles only'
);

// Add constraint
$constraint = PermissionConstraint::create(
    permissionId: $permission->id,
    constraintType: 'own_resource_only',
    constraintJson: json_encode(['field' => 'author_id'])
);

// Check permission with context
$context = ['resource_id' => $article->id, 'author_id' => $user->id];
$hasPermission = $permissionChecker->userHasPermissionWithContext(
    $user->id,
    $permission->id,
    $context
);

Audit Logging

<?php

// Log role assignment
$auditLogger->log(
    action: 'assign_role',
    userId: $adminUser->id,
    objectType: 'role',
    objectId: $role->id,
    details: [
        'assigned_to' => $user->id,
        'organization_id' => $organization->id
    ]
);

// Log permission check
$auditLogger->log(
    action: 'check_permission',
    userId: $user->id,
    objectType: 'permission',
    objectId: $permission->id,
    details: [
        'result' => $result->hasPermission,
        'resource' => 'article',
        'action' => 'read'
    ]
);

๐Ÿค Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ†˜ Support

๐Ÿ— Architecture Overview

src/
โ”œโ”€โ”€ Domain/                 # Business logic layer
โ”‚   โ”œโ”€โ”€ Entity/            # Domain entities
โ”‚   โ”œโ”€โ”€ ValueObject/       # Value objects (UUIDs, etc.)
โ”‚   โ”œโ”€โ”€ Repository/        # Repository interfaces
โ”‚   โ””โ”€โ”€ Service/           # Domain services
โ”œโ”€โ”€ Application/           # Application layer
โ”‚   โ”œโ”€โ”€ DTO/              # Data transfer objects
โ”‚   โ””โ”€โ”€ UseCase/          # Application use cases
โ”œโ”€โ”€ Infrastructure/        # Infrastructure layer
โ”‚   โ”œโ”€โ”€ Persistence/      # Repository implementations
โ”‚   โ”œโ”€โ”€ Security/         # Security implementations
โ”‚   โ””โ”€โ”€ Service/          # Service implementations
โ””โ”€โ”€ Shared/               # Shared components
    โ”œโ”€โ”€ Contracts/        # Shared interfaces
    โ””โ”€โ”€ Exceptions/       # Exception classes

This architecture follows Clean Architecture principles, ensuring:

  • Independence: Domain logic is independent of frameworks
  • Testability: Easy to unit test business logic
  • Flexibility: Easy to swap implementations
  • Maintainability: Clear separation of concerns