Security and Access Control
Authorization: The Idea
A single question covers the authorization with Smalldb state machines: Can a user invoke a given transition, or not?
Every data manipulation or other operation happens within a state machine transition only. Therefore, we have to worry only about the transitions. The state machine definition already tells us what users can do and when. To implement an access control mechanism, we need to extend the state machine definition with access control rules, and then evaluate the respective rule before invoking each transition.
Because the entire security model is defined in one place — the state machine definition, it is relatively easy to verify the correctness of the application’s security model. Moreover, the rules can be visualized in the state diagram (e.g., by changing the color of an arrow), making the verification even easier.
Controllers and views can check the availability of relevant transitions and show only available parts of a user interface to the user. We can also deny access to an entire controller if the user is not allowed to invoke a given transition of the state machine. This way, we can use state machine’s access control rules in the entire application, while keeping them all defined in a single source of truth.
Authorization: Example
The diagram shows an article state machine with two user roles. An author is allowed (hollow blue arrows) to write and submit an article. An editor is allowed (filled red arrows) to return the article to the author or reject/accept it for publishing. Once the article is in “Published”, “Rejected”, or “Waiting” state, the author cannot do anything with it because no blue arrows are leading from these states (and the author does not see the red arrows).
Note that while the editor is defined using a simple role-based condition, the authorship is a relation between a particular article and its author. The “Create” transition is available to all users (black arrow) because there is no author to a non-existent article.
From a formal analysis perspective, the otherwise tiresome properties, like state reachability or liveness of a state machine, are much more interesting when access control is in play. We can use these properties to make sure users do not get stuck somewhere and reach their goals.
Access Control Extension
Smalldb state machine delegates the implementation of access control rules to a Transition Guard,
which implements the TransitionGuard
interface and is injected into each transition decorator.
Access Control Extension implements such a transition guard and adds an extension to state machine definition to represent the access control rules.
The access control is then about two annotations @AC\DefinePolicy
, and @AC\UsePolicy
:
use Smalldb\StateMachine\AccessControlExtension\Annotation\AC;
use Smalldb\StateMachine\Annotation\StateMachine;
use Smalldb\StateMachine\Annotation\Transition;
use Smalldb\StateMachine\StyleExtension\Annotation\Color;
/**
* @StateMachine("article")
* @AC\DefinePolicy("Author", @AC\IsOwner("authorId"), @Color("blue"))
* @AC\DefinePolicy("Editor", @AC\IsGranted("ROLE_ADMIN"), @Color("red"))
* ...
*/
abstract class Article
{
/**
* @Transition
* @AC\UsePolicy("Author")
*/
abstract public function submit(/*...*/);
// ...
/**
* @Transition
* @AC\UsePolicy("Editor")
*/
abstract public function accept(/*...*/);
// ...
}
The @AC\DefinePolicy
defines a policy of the given name and predicate.
Such a policy allows the user to invoke a transition when the predicate is true.
In our small example, we define the “Author” access policy that allows a transition when autorId
of the article matches the ID of currently logged in user.
Also, we define that such a transition should be rendered in blue.
The second access control policy, the “Editor”, uses Symfony’s IsGranted
feature to ensure that the user is granted ROLE_ADMIN
before invoking a transition.
We may specify more complex predicates using @AC\AllOf()
, @AC\SomeOf()
, and @AC\NoneOf()
operators.
For example, we may define the Author using the following predicate: @AC\AllOf(@AC\IsOwner("authorId"), @AC\IsGranted("IS_AUTHENTICATED_FULLY"))
(A transition is allowed if the user is the owner of the article and is fully authenticated.)
Once we have the access policies defined, we assign them to the individual transitions using the @AC\UsePolicy()
annotations.
We refer to the access policies using their names only.
We may specify the default access policy using @AC\DefaultPolicy("Author")
class annotation
so that we don’t have to define such a default policy for each transition.
Predicates and DI Container
The Access Control Extension compiles the predicates from annotations into services in the Symfony’s DI container.
There are three classes involved: a PredicateAnnotation
class adds a Predicate
class into the state machine definition,
which then compiles into PredicateCompiled
service and gets registered in the container.
The trick is that the PredicateCompiled
service can be injected with other services.
For example, the @AC\IsGranted
annotation adds IsGranted
predicate to the definition, which
then compiles into IsGrantedCompiled
service, which requires Symfony’s AuthorizationCheckerInterface
(as a constructor argument)
so that it can evaluate the "ROLE_ADMIN"
and "IS_AUTHENTICATED_FULLY"
.
Thanks to a service inlining optimization in the Symfony DI container, the result is a quite effective PHP code with minimal overhead.
@IsGranted — Symfony integration
The @AC\IsGranted
annotation and Symfony’s @IsGranted
annotations are not the same.
The @AC\IsGranted
asks Symfony whether the user is granted a role.
We use this annotation in state machine definition to restrict access to a transition when the user does not have a role.
The Symfony’s @IsGranted
can ask Smalldb whether a transition is allowed (via a SmalldbVoter
).
We use this annotation in controllers to restrict access when the user is not allowed to invoke a transition.
For example, we may use @IsGranted("article!create")
in a controller
and @AC\IsGranted("IS_AUTHENTICATED_FULLY")
in the state machine definition.
The controller will ask the state machine if the user can invoke the create
transition on the article
state machine.
Then the state machine will ask Symfony if the user is fully authenticated.
If we later decide to change the policy of creating the articles, we need to change the policy in the state machine definition only.
Demo
For a working example, please refer to the Smalldb Demo Application. We do not wish to complicate the “To Do List” application with user login and management, which is required for a proper demonstration of the access control features.