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.
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']));
}
}
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.
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.
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.
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');
}
}
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');
}
}
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.*.*');
// ...
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.
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
andForbidRule
are always returningtrue
andfast
accordingly.
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;
}
}
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';
}
}
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';
}
}