The Role-Based Access Control (RBAC) component provides role-based authorization abstraction for the Devflow CMF.

Introduction

Role-Based Access Control (RBAC) is based on the idea of roles rather than permissions as you may find in ACL. In a web application, users will typically have identities defined by username, email, token, etc.

RBAC System:

  • An Identity has one or more roles
  • A role requests access to a permission
  • A permission is given to a role

Thus, RBAC has:

  • Many-to-many relationship between identities and roles.
  • Many-to-many relationship between roles and permissions.
  • Roles can have a parent role.

Rules

Sometimes you need to perform an extra check. For example, what if you only want authors to edit, update or delete their own content, but not someone else's content? You can do that by setting a rule. You can do so by implementing the AssertionRule interface with the execute() method.

<?php

declare(strict_types=1);

namespace Infrastructure\Service;

use Codefy\Framework\Auth\Rbac\Entity\AssertionRule;

final class AuthorRule implements AssertionRule
{

    /**
     * @param array|null $params
     *
     * @return bool
     */
    public function execute(?array $params = null): bool
    {
        // @var Post $post
        if($post = $params['post'] ?? null) {
            return $post->authorId === ($params['userId'] ?? null);
        }
        return false;
    }
}

File: ./Cms/Infrastructure/Service/AuthorRule.php

Configure RBAC

<?php

return [
    /** Named or grouped permissions. */
    'permissions' => [
        'system' => [
            'description' => 'System permissions',
            'permissions' => [
                'access:admin' => ['description' => 'Access to the dashboard.'],
                'manage:media' => ['description' => 'Manage media.'],
                'manage:settings' => ['description' => 'Manage settings.'],
                'change:settings' => ['description' => 'Update or delete settings.'],
                'manage:options' => ['description' => 'Manage plugin or theme options.'],
                'change:options' => ['description' => 'Update or delete plugin or theme options.'],
                'manage:profile' => ['description' => 'User can manage their profile.'],
                'core:updates' => ['description' => 'Manage system updates.'],
                'flush:cache' => ['description' => 'Flush the cache.'],
                'change:username' => ['description' => 'Update a user username.'],
                'reset:password' => ['description' => 'Can reset a user password.'],
            ],
        ],
        'content' => [
            'description' => 'Content permissions',
            'permissions' => [
                'create:content' => ['description' => 'Create content.'],
                'manage:content' => ['description' => 'Manage content.'],
                'update:content' => ['description' => 'Update content.'],
                'delete:content' => ['description' => 'Delete content.'],
                'publish:content' => ['description' => 'Publish content.'],
                'schedule:content' => ['description' => 'Schedule content for publishing.'],
                'archive:content' => ['description' => 'Archive content.'],
                'review:content' => ['description' => 'Review content for publishing.'],
                'assign:content_reviewers' => ['description' => 'Assign content to reviewers.'],
                'approve:content' => ['description' => 'Approve content for publishing.'],
                'view:content_revisions' => ['description' => 'View content revisions.'],
                'restore:content_revisions' => ['description' => 'Restore content revisions.'],
                'view:content_activity' => ['description' => 'View content activity.'],
                'manage:content_notifications' => ['description' => 'Manage content notifications.'],
                'view:content_comments' => ['description' => 'View content comments.'],
                'comment:content' => ['description' => 'Manage comments.'],
                'edit:content_comments' => ['description' => 'Edit content comments.'],
                'reply:content_comments' => ['description' => 'Reply to content comments.'],
                'resolve:content_comments' => ['description' => 'Resolve content comments.'],
                'delete:content_comments' => ['description' => 'Delete content comments.'],
            ],
        ],
        'product' => [
            'description' => 'Product permissions',
            'permissions' => [
                'create:product' => ['description' => 'Create product.'],
                'manage:product' => ['description' => 'Manage product.'],
                'manage:products' => ['description' => 'Manage product inventory.'],
                'update:product' => ['description' => 'Update product.'],
                'delete:product' => ['description' => 'Delete product.'],
                'publish:product' => ['description' => 'Publish product.'],
                'review:product' => ['description' => 'Review product for publishing.'],
                'archive:product' => ['description' => 'Archive product.'],
            ],
        ],
        'user' => [
            'description' => 'User permissions',
            'permissions' => [
                'create:users' => ['description' => 'Create users.'],
                'manage:users' => ['description' => 'Manage users.'],
                'update:users' => ['description' => 'Update users.'],
                'delete:users' => ['description' => 'Delete users.'],
                'switch:user' => ['description' => 'Switch user'],
            ],
        ],
        'extensions' => [
            'description' => 'Extension permissions',
            'permissions' => [
                'install:extensions' => ['description' => 'Install any extension.'],
                'uninstall:extensions' => ['description' => 'Uninstall any extension.'],
                'manage:plugins' => ['description' => 'Manage plugins.'],
                'manage:themes' => ['description' => 'Manage themes.'],
                'install:themes' => ['description' => 'Install themes.'],
                'activate:themes' => ['description' => 'Activate themes.'],
                'deactivate:themes' => ['description' => 'Deactivate themes.'],
                'install:plugins' => ['description' => 'Install plugins.'],
                'activate:plugins' => ['description' => 'Activate plugins.'],
                'deactivate:plugins' => ['description' => 'Deactivate plugins.'],
            ],
        ],
        'sites' => [
            'description' => 'All site permissions',
            'permissions' => [
                'manage:sites' => ['description' => 'Manage sites.'],
                'create:sites' => ['description' => 'Create sites.'],
                'update:sites' => ['description' => 'Update sites.'],
                'delete:sites' => ['description' => 'Delete sites.'],
            ]
        ],
        'webmaster' => [
            'description' => 'Website creator and builder.',
            'permissions' => [
                'vihzhuo:manage' => ['description' => 'Can use the Vihzhuo pagebuilder to build pages.'],
            ],
        ],
    ],

    'roles' => [
        'super' => [
            'description' => 'Super administrator',
            'permissions' => ['system','content','product','user','extensions','sites','webmaster'],
        ],
        'admin' => [
            'description' => 'Administrator',
            'permissions' => [
                'access:admin',
                'manage:media',
                'manage:options',
                'change:options',
                'manage:settings',
                'change:settings',
                'manage:profile',
                'flush:cache',
                'change:username',
                'reset:password',
                'content',
                'product',
                'user',
                'extensions',
                'webmaster',
            ],
        ],
        'author' => [
            'description' => 'Author who can create and edit drafts, but cannot publish.',
            'permissions' => [
                'access:admin',
                'manage:content',
                'create:content',
                'update:content',
                'review:content',
                'view:content_activity',
                'view:content_comments',
                'view:content_revisions',
                'comment:content',
                'reply:content_comments',
                'manage:media',
                'manage:profile',
                'flush:cache',
            ],
        ],
        'editor' => [
            'description' => 'Site editor',
            'permissions' => [
                'access:admin',
                'manage:content',
                'update:content',
                'review:content',
                'approve:content',
                'schedule:content',
                'assign:content_reviewers',
                'view:content_activity',
                'view:content_revisions',
                'view:content_comments',
                'comment:content',
                'reply:content_comments',
                'edit:content_comments',
                'resolve:content_comments',
                'manage:product',
                'manage:products',
                'update:product',
                'review:product',
                'manage:media',
                'manage:profile',
                'flush:cache',
            ],
        ],
        'publisher' => [
            'description' => 'Can publish approved content and archive published content.',
            'permissions' => [
                'access:admin',
                'manage:content',
                'update:content',
                'schedule:content',
                'publish:content',
                'archive:content',
                'view:content_activity',
                'view:content_revisions',
                'view:content_comments',
                'comment:content',
                'reply:content_comments',
                'resolve:content_comments',
                'manage:media',
                'manage:profile',
                'flush:cache',
            ],
        ],
        'user' => [],
    ],
];

File: ./config/rbac.php

Check Rights

<?php

use function App\Shared\Helpers\current_user_can;

if(current_user_can('delete:content', ['userId' => $userId, 'post' => $post]) {
    ... // The user is author of the post and can delete it
}

Authorizing Users

Devflow provides several middlewares for checking and validating user roles, permissions, and session handling.

If you need to check whether a user is authorized to view a certain page, you will need to add
Codefy\Framework\Http\Middleware\Auth\UserAuthorizationMiddleware or the Injector alias (user.authorization) to your route:

<?php

declare(strict_types=1);

return (function(\Qubus\Routing\Psr7Router $router) {
    $router->get('/protected/', function() {
        //
    })
    ->middleware('user.authorization')
    ->name('protected.page');
});

File: ./routes/web/web.php

When a user visits the /protected/ route, the middleware will check if the user is logged in. If the user is logged in, the user will continue on, otherwise, the user will be redirected to your login route via the redirect_guests_to setting in ./config/auth.php.

A different approach would be to check permissions via the controller. You can use the App\Shared\Helpers\current_user_can helper or the Gate Middleware:

<?php

declare(strict_types=1);

namespace Application\Http\Controller;

use App\Application\Devflow;
use Psr\Http\Message\ResponseInterface;
use Qubus\Routing\Psr7Router;

use function App\Shared\Helpers\current_user_can;
use function Codefy\Framework\Helpers\trans;
use function Codefy\Framework\Helpers\view;
use function Qubus\Routing\Helpers\redirect;

final class ProtectedController
{
    public function __construct(protected Psr7Router $router)
    {
    }

    public function securePage(): ResponseInterface
    {
        if (false === current_user_can('manage:content')) {
            Devflow::$PHP->flash->error(
                message: 'Access denied.'
            );

            return redirect($this->router->url(name: 'protected.page'));
        }

        return view(
            template: 'cmf::backend/protected',
            data: ['title' => trans('Protected Page')]
        );
    }
}

File: ./Cms/Application/Http/Controller/ProtectedController.php

If you use the gate middleware, then your controller becomes cleaner by adding the gate middleware to your route:

<?php

declare(strict_types=1);

use Application\Http\Controller\ProtectedController;

return (function(\Qubus\Routing\Psr7Router $router) {
    $router->get('/protected/', function(ProtectedController $controller) {
        return $controller->securePage();
    })
    ->middleware(['gate:manage:content,/admin/login/']);
});

File: ./routes/web/admin.php

<?php

declare(strict_types=1);

namespace Application\Http\Controller;

use Psr\Http\Message\ResponseInterface;

use function Codefy\Framework\Helpers\trans;
use function Codefy\Framework\Helpers\view;

final class ProtectedController
{
    public function securePage(): ResponseInterface
    {
        return view(
            template: 'cmf::backend/protected',
            data: ['title' => trans('Protected Page')]
        );
    }
}

File: ./Cms/Application/Http/Controller/AdminController.php

current_user_can('manage:content') is what's used to check if a logged-in user has a certain permission to continue.

Here is a list of other authentication middlewares along with their aliases:

  • Codefy\Framework\Http\Middleware\Auth\ExpireUserSessionMiddleware
    • Alias: user.session.expire
    • Description: This middleware can be used for a logout route to clear the user's session and cookie.
  • Codefy\Framework\Http\Middleware\Auth\AuthenticationMiddleware
    • Alias: user.authenticate
    • Description: This middleware can be used for a login route which checks the submitted login credentials against the database.
  • Codefy\Framework\Http\Middleware\Auth\UserSessionMiddleware
    • Alias: user.session
    • Description: This middleware should be used with the previous middleware. If authentication is successful, the user session and cookie will be created.