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.
The implementation includes multiple additions such as:
Note
Such additions make it possible to use the component as the framework for ACL, DAC, and ABAC security models.
Make sure to enable Spiral\Bootloader\Security\GuardBootloader
to activate the component, no configuration is
required.
Note
The component is enabled in Web and GRPC bundles by default.
All the privileges within the application will be granted based on the roles associated with
the current Spiral\Security\ActorInterface
:
interface ActorInterface
{
public function getRoles(): array;
}
Note
Use IoC scopes to set Actor during the user request.
Read how to use the authenticated user as an actor here.
By default, the application uses 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): string
{
return $scope->runScope([ActorInterface::class => new Actor(['admin'])], function (): string {
// the actor has role `admin` in this scope
return 'ok';
});
}
}
Note
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 the 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): void
{
$container->bindSingleton(ActorInterface::class, new Actor(['user']));
}
}
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): void
{
$container->bindSingleton(ActorInterface::class, new Actor(['user']));
$rbac->addRole('user');
$rbac->associate('user', 'home.read');
}
}
Note
The role-rule-permission association will be explained in detail down below.
Once the Bootloader is activated, you can use 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): string
{
if (!$guard->allows('home.read')) {
return 'can not read';
}
return 'can read';
}
}
Note
Change the default actor roles to see how it affects the result.
Use guard
prototype property to develop faster.
namespace App\Controller;
use Spiral\Prototype\Traits\PrototypeTrait;
class HomeController
{
use PrototypeTrait;
public function index(): string
{
if (!$this->guard->allows('home.read')) {
return 'can not read';
}
return 'can read';
}
}
Note
You can useGuardInterface
in controllers, services, and views.
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 the current Actor is trying to access or edit.
namespace App\Controller;
use Spiral\Security\GuardInterface;
class HomeController
{
public function index(GuardInterface $guard): string
{
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.
The core part of the RBAC component is Spiral\Security\PermissionInterface
. While you can use your implementation
with a dynamic role and permission configuration, by default, it's intended to configure mapping in the Bootloader.
Every application must register available user roles in the RBAC component. Use the method addRole
:
namespace App\Bootloader;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Security\PermissionsInterface;
class SecurityBootloader extends Bootloader
{
public function boot(PermissionsInterface $rbac): void
{
$rbac->addRole('guest');
}
}
Once the role or roles are 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): void
{
$rbac->addRole('guest');
$rbac->associate('guest', 'home.read');
}
}
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 the namespace level can be associated at once, if you use deep namespaces with .
separator:
$rbac->associate('guest', 'home.*');
$rbac->associate('guest', 'home.*.*');
// ...
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): void
{
$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): void
{
$rbac->addRole('guest');
$rbac->associate('guest', 'home.*', Rule\AllowRule::class);
$rbac->associate('guest', 'home.read', Rule\ForbidRule::class);
}
}
Note
The Guard will check all of the actor roles, at least one of them must grant the permission.
As mentioned above, all of the role-to-permission associations are 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.
Note
The default rulesAllowRule
andForbidRule
are always returningtrue
andfast
accordingly.
To create a 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): void
{
$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(): string
{
if ($this->guard->allows('home.read', ['key' => 'value'])) {
echo 'yay';
}
if ($this->guard->allows('home.read', ['key' => 'else'])) {
echo 'nope';
}
}
}
Note
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 is an author:
class SampleRule implements RuleInterface
{
public function allows(ActorInterface $actor, string $permission, array $context): bool
{
return $context['post']->user === $actor;
}
}
To simplify the rule creation, use Spiral\Security\Rule
, which enables the method injection with the
method check
.
namespace App\Security;
use Spiral\Security\Rule;
class SampleRule extends Rule
{
public function check(string $key): bool
{
return $key === 'value';
}
}
Note
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';
}
}
You can use Guarded
attribute to automatically check the access to the controller methods using
the Interceptors.
namespace App\Controller;
use Spiral\Domain\Annotation\Guarded;
class HomeController
{
#[Guarded(permission: 'home.index', else: 'notFound')]
public function index(): string
{
return 'OK';
}
}