Skip to content

Latest commit

 

History

History
447 lines (337 loc) · 11.5 KB

rbac.md

File metadata and controls

447 lines (337 loc) · 11.5 KB

Security - Role-Based Access Control

The framework includes the component spiral/security, which provides the ability to authorize user/actor access to the specific resources or actions based on the list of associated privileges. The components implements "Flat RBAC" patterns described in NIST RBAC research.

Implementation includes multiple additions such as:

  • an additional layer of rules to control the privilege/permission context
  • the ability to assign a role to multiple privileges using wildcard pattern
  • the ability to overwrite role-to-permission assignment using higher priority rule

Such additions make it possible to use the component as the framework for ACL, DAC, and ABAC security models.

Make sure to enable the Spiral\Bootloader\Security\GuardBootloader to activate the component, no configuration is required.

The component is enabled in Web and GRPC bundles by default.

Actor

All the privileges within the application will be granted based on roles associated with current Spiral\Security\ActorInterface:

interface ActorInterface
{
    public function getRoles(): array;
}

Use IoC scopes to set Actor during the user request.

Read how to use authenticated user as an actor here.

By default, the application use Spiral\Security\Actor\Guest as the default actor. You can set the Actor globally or inside IoC scope using the container binding.

namespace App\Controller;

use Spiral\Core\ScopeInterface;
use Spiral\Security\Actor\Actor;
use Spiral\Security\ActorInterface;

class HomeController
{
    public function index(ScopeInterface $scope)
    {
        return $scope->runScope([ActorInterface::class => new Actor(['admin'])], function () {

            // the actor has role `admin` in this scope
            return 'ok';
        });
    }
}

You can set the active Actor using domain core interceptors, GRPC interceptors, HTTP middleware, custom IoC scopes, etc.

For the simplicity of this guide, we will see the default actor globally, via custom Bootloader:

namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\Container;
use Spiral\Security\Actor\Actor;
use Spiral\Security\ActorInterface;

class SecurityBootloader extends Bootloader
{
    public function boot(Container $container)
    {
        $container->bindSingleton(ActorInterface::class, new Actor(['user']));
    }
}

GuardInterface

To use the RBAC component, we must register available roles and create an association between the role and the permission, use the same Bootloader, and Spiral\Security\PermissionsInterface for this purpose.

namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\Container;
use Spiral\Security\Actor\Actor;
use Spiral\Security\ActorInterface;
use Spiral\Security\PermissionsInterface;

class ActorBootloader extends Bootloader
{
    public function boot(Container $container, PermissionsInterface $rbac)
    {
        $container->bindSingleton(ActorInterface::class, new Actor(['user']));

        $rbac->addRole('user');
        $rbac->associate('user', 'home.read');
    }
}

The role-rule-permission association will be explained in detail down below.

Once the Bootloader is activated, you can use the Spiral\Core\GuardInterface to check the access to the specific permissions, the guard will resolve active Actor automatically via dynamic scope using Spiral\Security\GuardScope. The interface provides the method allows which we can use to check if the Actor has access to the specific permission:

namespace App\Controller;

use Spiral\Security\GuardInterface;

class HomeController
{
    public function index(GuardInterface $guard)
    {
        if (!$guard->allows('home.read')) {
            return 'can not read';
        }

        return 'can read';
    }
}

Change the default actor roles to see how it affects the result.

Use the guard prototype property to develop faster.

namespace App\Controller;

use Spiral\Prototype\Traits\PrototypeTrait;

class HomeController
{
    use PrototypeTrait;

    public function index()
    {
        if (!$this->guard->allows('home.read')) {
            return 'can not read';
        }

        return 'can read';
    }
}

You can use GuardInterface in controllers, services, and views.

Permission Context

The allows method of the guard object supports the second argument which defines the permission context, usually it must contain the instance of the target entity which current Actor is trying to access or edit.

namespace App\Controller;

use Spiral\Security\GuardInterface;

class HomeController
{
    public function index(GuardInterface $guard)
    {
        if (!$guard->allows('home.read', ['key' => 'value'])) {
            return 'can not read';
        }

        return 'can read';
    }
}

Down below, we will explain how to use the context to create more complex role-permission associations.

Permissions Management

The core part of the RBAC component is Spiral\Security\PermissionInterface. While you can use your implementation with dynamic role and permission configuration, by default, it's intended to configure mapping in the Bootloader.

Create Role

Every application must register available user roles in the RBAC component, use method addRole:

namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Security\PermissionsInterface;

class SecurityBootloader extends Bootloader
{
    public function boot(PermissionsInterface $rbac)
    {
        $rbac->addRole('guest');
    }
}

Permission

Once the role or roles created, you can associate them to the permission using the method associate:

namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Security\PermissionsInterface;

class SecurityBootloader extends Bootloader
{
    public function boot(PermissionsInterface $rbac)
    {
        $rbac->addRole('guest');
        
        $rbac->associate('guest', 'home.read');
    }
}

Wildcard Association

You can associate the role to more than one permission at once:

$rbac->associate('guest', 'home.(read|write)');

You can also use the * pattern to create the association with the whole namespace of permissions:

$rbac->associate('guest', 'home.*');

Note, only namespace level can be associated at one, if you use deep namespaces with . separator:

$rbac->associate('guest', 'home.*');
$rbac->associate('guest', 'home.*.*');
// ...

Exclude Permission

In some cases, you need to grant access to all of the namespace permissions, excluding specific ones. Overwrite the permissions using the 3rd argument, which can specify the Rule. To forbid role access use Spiral\Security\Rule\ForbidRule:

namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Security\PermissionsInterface;
use Spiral\Security\Rule\ForbidRule;

class SecurityBootloader extends Bootloader
{
    public function boot(PermissionsInterface $rbac)
    {
        $rbac->addRole('guest');

        $rbac->associate('guest', 'home.*');
        $rbac->associate('guest', 'home.read', ForbidRule::class);
    }
}

The default rule used to create an association is AllowRule. The previous example can be explained better using:

namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Security\PermissionsInterface;
use Spiral\Security\Rule;

class SecurityBootloader extends Bootloader
{
    public function boot(PermissionsInterface $rbac)
    {
        $rbac->addRole('guest');

        $rbac->associate('guest', 'home.*', Rule\AllowRule::class);
        $rbac->associate('guest', 'home.read', Rule\ForbidRule::class);
    }
}

The Guard will check all of the actor roles, at least one of them must grant the permission.

Rules

As mentioned above, all of the role-to-permission associated controlled using a set of rules. Each Rule must implement Spiral\Security\RuleInterface.

interface RuleInterface
{
    public function allows(ActorInterface $actor, string $permission, array $context): bool;
}

While checking the role access to the permission, the rule will receive the current context provided by GuardInterface->allows method:

if (!$guard->allows('home.read', ['key' => 'value'])) {
    return 'can not read';
}

Such an approach allows you to create a complex rule association between the role and the set of permissions.

The default rules AllowRule and ForbidRule are always returning true and fast accordingly.

Custom Rules

To create custom rule simply implement the Spiral\Security\RuleInterface interface, for example, we can create the Rule which will only allow access when the context has key equals value:

namespace App\Security;

use Spiral\Security\ActorInterface;
use Spiral\Security\RuleInterface;

class SampleRule implements RuleInterface
{
    public function allows(ActorInterface $actor, string $permission, array $context): bool
    {
        return $context['key'] === 'value';
    }
}

You can assign this Rule to the role-to-permission association using class name:

namespace App\Bootloader;

use App\Security\SampleRule;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Security\PermissionsInterface;

class SecurityBootloader extends Bootloader
{
    public function boot(PermissionsInterface $rbac)
    {
        $rbac->addRole('guest');

        $rbac->associate('guest', 'home.*', SampleRule::class);
    }
}

Now, changing the context of allows method will cause either approval or denial:

namespace App\Controller;

use Spiral\Prototype\Traits\PrototypeTrait;

class HomeController
{
    use PrototypeTrait;

    public function index()
    {
        if ($this->guard->allows('home.read', ['key' => 'value'])) {
            echo 'yay';
        }

        if ($this->guard->allows('home.read', ['key' => 'else'])) {
            echo 'nope';
        }
    }
}

Use the actor interface to create more complex rules.

Pass domain entities as context to check the authority:

if ($this->guard->allows('post.edit', ['post' => $post])) {
    echo 'yay';
}

And the Rule to check if the user if author:

class SampleRule implements RuleInterface
{
    public function allows(ActorInterface $actor, string $permission, array $context): bool
    {
        return $context['post']->user === $actor;
    }
}

Abstract Rule

To simplify the rule creation, use the Spiral\Security\Rule, which enables the method injection with on method check.

namespace App\Security;

use Spiral\Security\Rule;

class SampleRule extends Rule
{
    public function check(string $key): bool
    {
        return $key === 'value';
    }
}

You can access to any application services via method injection.

We recommend declaring rules as singletons to improve the performance of the application.

namespace App\Security;

use Spiral\Core\Container\SingletonInterface;
use Spiral\Security\Rule;

class SampleRule extends Rule implements SingletonInterface
{
    public function check(string $key): bool
    {
        return $key === 'value';
    }
}

@Guarded Annotation

You can use Guarded annotation to automatically check the access to the controller methods using the domain cores.

namespace App\Controller;

use Spiral\Domain\Annotation\Guarded;

class HomeController
{
    /**
     * @Guarded("home.index", else="notFound")
     */
    public function index()
    {
        return 'OK';
    }
}