feruzlabs / rbac
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
Requires
- php: >=8.3
- ramsey/uuid: ^4.7
Requires (Dev)
- phpunit/phpunit: ^10.0
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
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Support
- Documentation: GitHub Wiki
- Issues: GitHub Issues
- Discussions: GitHub Discussions
๐ 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