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

Article with permissions

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.


See also …