The Spiral Framework includes Security component based on role-rule-permission structure.
The Security component work using set of roles associated with different application permissions (i.e. ability to edit post) based on set of context specific rules (how to allow specific permission).
In order to configure security component in your application use boot
method of custom bootloader:
class AccessBootloader extends Bootloader
{
const BOOT = true;
const BINDINGS = [
ActorInterface::class => [self::class, 'getActor']
];
/**
* @param \Spiral\Security\PermissionsInterface $permissions
*/
public function boot(PermissionsInterface $permissions)
{
$permissions->addRole('admin');
$permissions->addRole('manager');
//Grant admin full assess to 3 levers of permissions
$permissions->associate('admin', "*");
$permissions->associate('admin', "*.*");
$permissions->associate('admin', "*.*.*");
//Grant manager only access to vault dashboard
$permissions->associate('manager', "vault");
$permissions->associate('manager', "vault.dashboard");
}
//Use authorizer user as actor (see authorization)
public function getActor(ContextInterface $context): ActorInterface
{
if ($context->isAuthenticated()) {
return $context->getUser();
}
return new Guest();
}
}
Boot method in a given sample creates 2 roles "admin" and "manager" and associates such roles with application permissions.
Security Component provides ability to associate role and rule with multiple permissions using * patter, for example we can grant ability for admin for all post specific permissions:
$permissions->associate('admin', "posts.*");
Manager on other end only have associated with 2 direct permissions.
Security component requires active IoC scope for application actor (i.e. ActorInterface
):
interface ActorInterface
{
/**
* Method must return list of roles associated with current actor is a form of array.
*
* @return array
*/
public function getRoles(): array;
}
Actors usually represent by authenticated users which demonstrated in getActor
factory method.
To guard our code in services or controllers use GuardTrait or request GuardInterface
dependency:
use GuardedTrait;
public function indexAction()
{
if(!$this->getGuard()->allows('post.edit')) {
throw new ForbiddenException();
}
}
You are able to use shorter method in your controllers by using AuthorizesTrait
:
use AuthorizesTrait;
public function indexAction()
{
//Will throw ControllerException when failed
$this->authorize('posts.edit');
}
Previous example used default role-permissions association without any of additional conditions.
Practically we can rewrite association using following code:
$permissions->associate('admin', "*", AllowRule::class);
When AllowRule looks like:
final class AllowRule implements RuleInterface, SingletonInterface
{
/**
* {@inheritdoc}
*/
public function allows(ActorInterface $actor, string $permission, array $context): bool
{
return true;
}
}
To create custom rules which can work based on a context, let's extend default class Rule to get access to method injections in our rules:
class AuthorRule extends Rule
{
public function check(User $user, Post $post)
{
return $post->author->id == $actor->id;
}
}
Check this file to see how Rule class works.
This rule grants access when user is post owner, we can register it like that:
$permissions->associate('user', "post.edit", AuthorRule::class);
In order to use this rule properly we have to supply additional context "post" (actor/user will be resolved automatically):
$this->authorize('posts.edit', ['post' => $post]);
Check RuleInterface class in order to understand how to create more rules, all rules are initiated by Container which allows __constructor
injections:
interface RuleInterface
{
/**
* @param ActorInterface $actor
* @param string $permission
* @param array $context
*
* @return bool
*
* @throws RuleException
*/
public function allows(ActorInterface $actor, string $permission, array $context): bool;
}
Security component ships with set of pre-defined rules:
Rule | Description |
---|---|
AllowRule | Always grant access. |
ForbidRule | Always forbid access |
CallableRule | Provides ability to wrap external functions and closures |
CompositeRule | Combines multiple rules together |
Example of composite rule definition:
class PostRule extends CompositeRule
{
const BEHAVIOUR = self::AT_LEAST_ONE;
const RULES = [AdminRule::class, AuthorRule::class];
}
This rule will grant permission access if user is Admin or Post author.
You can use pre-build implementation of HMVC core - Vault which includes automatic authorization for each of it nested controllers and actions.