Video Tutorial Create a permissions system


In this tutorial I suggest you think about setting up a permissions system in PHP. The objective is to set up a system that will allow us to check if the user is authorized to perform a specific action within our application.

00:00 Presentation of existing permission systems
09:50 We create our own system

The strategies

Through my exploration of different frameworks / technologies I was able to discover different approaches to the problem.

Hierarchical permissions

This strategy consists of creating different roles by assigning them a specific number (the larger the number, the higher the permission).

const ROLE_ADMIN = 100
const ROLE_MODERATEUR = 10
const ROLE_USER = 1

Users are then assigned a level based on these constants. You can then use this level to control access to a feature.

if ($ user-> role <ROLE_MODERATEUR) {
    throw new ForbiddenException ();
}
// We do the treatment

This approach is sufficient for simple cases but is limited for more complex cases, especially when logic is added to the checks (for example a user can only modify his articles, but an administrator can edit all the articles) or when the permissions are not hierarchical.

// The permission logic becomes more and more complex over time
if ($ user-> role> = ROLE_ADMIN || ($ user-> role> = ROLE_CONTRIBUTEUR && $ post-> userId === $ user-> id)) {
    throw new ForbiddenException ();
}
// We do the treatment

The roles

Another idea is to create roles for users and associate a series of permissions with these roles.

$ permissions = (
    'ROLE_ADMIN' => (
        'can_edit_post',
        'can_update_post',
        'can_create_post',
        'can_read_post',
    ),
    'ROLE_USER' => (
        'can_read_post'
    )
)

We can also add conditions if we want more flexibility in the conditions of access to a certain permissions.

$ permissions = (
    'ROLE_ADMIN' => (
        'can_update_post',
        'can_create_post',
        'can_delete_post',
        'can_read_post',
    ),
    'ROLE_CONTRIBUTEUR' => (
        'can_update_post' => function (User $ user, Post $ post) {return $ post-> user-> id === $ user-> id; }
        'can_create_post',
        'can_read_post'
    )
    'ROLE_USER' => (
        'can_read_post' => function (User $ user, Post $ post) {return $ post-> isOnline; }
    )
    'ROLE_ANONYMOUS' => (
        'can_read_post' => function (User $ user, Post $ post) {return $ post-> isPublic; }
    )
)

This approach is already much more interesting because it makes it possible to manage completely different roles and the user can even be assigned several roles. On the other hand, this implies creating an object that will contain all of the permissions upstream and when the application becomes more complex the number of permissions / conditions can become difficult to manage.

The capacities

This approach is used by the CanCanCan library and consists of defining the capacities offered to upstream users based on their profile or role.

class Ability
  include CanCan :: Ability

  def initialize (user)
    can: read, Post, public: true

    if user.present? # additional permissions for logged in users (they can read their own posts)
      can: read, Post, user_id: user.id

      if user.admin? # additional permissions for administrators
        can: read, Post
      end
    end
  end
end

This approach is very similar to the previous approach but the declaration of permissions is done in a different way by associating with the name of the permission the object which is the target of the request. A PHP version would look like this:

class Ability {

    public function __construct (? User $ user = null) {

        $ this-> can ('read', Post :: class, ('public' => true));

        if ($ user! == null) {
            $ this-> can ('read', Post :: class, ('user_id' => $ user-> id));

            if ($ user-> isAdmin) {
                $ this-can ('read', Post :: class)
            }
        }
    }

}

As for the previous method the problem is the multiplication of the rules during the increase in complexity of the permissions and it can be complicated to find one's way in the definition of the rules / conditions.

Policies

This approach is visible on the Laravel framework and consists in defining an access policy system. This approach allows you to focus on the target of the permission request rather than focusing on the user.

isAdmin || $ user-> isContributor;
    }

    public function update (User $ user, Post $ post)
    {
        return ($ user-> isContributor && $ user-> id === $ post-> userId) || $ user-> isAddmin;
    }

    public function read (User $ user, Post $ post)
    {
        return $ post-> isPublic;
    }
}

Once this policy is defined it can be associated with a class (often a model):

class AuthServiceProvider extends ServiceProvider
{
    protected $ policies = (
        Post :: class => PostPolicy :: class,
    );

    public function boot () {
        $ this-> registerPolicies ();
    }
}

Thus, when the target of a permission is an article, the method corresponding to the name of the permission will be called in order to give (or not) the authorization to the user.

Gate :: authorize ('update', $ post); // Throw an exception if the result of PostPolicy :: update is false

This approach is interesting because it makes it possible to create generic policies which can be applied to several models. On the other hand, transverse permissions are always problematic (the super administrator has access to all the site for example) and it will be necessary to supplement this system with a more traditional system similar to CanCanCan. Laravel offers a gate system imitating the previous approach.

Gate :: define ('edit-settings', function ($ user) {
    return $ user-> isAdmin;
});

The voters

This system is more basic than the previous ones and consists in defining the management of permissions as a voting system. We start by recording a series of Vote in our app. When permission is requested all of the Vote will be consulted and they will indicate whether they participate in the vote or not. The Vote who participate will then vote to agree or not to agree to the requested permission. Finally, a reconciliation policy will be used to define whether permission is granted or not. There are several reconciliation strategies

  • Affirmative. Permission is granted once a Vote vote YES.
  • Unanimous. Permission is granted if all Vote vote YES.
  • Consensus. Permission is granted if the Vote voter YES are in the majority.

Setting up such a system is very simple. The interface of a vote allows him to declare his participation in a vote, and the result of his vote

<? php declare (strict_types = 1);

Voting interface
{

    public function canVote (string $ permission, $ subject = null): bool;

    public function vote (User $ user, string $ permission, $ subject = null): bool;

}

The class used to check permissions (we use the strategy here Affirmative) will be composed of a method allowing the recording of Vote and a method for consulting them for an authorization request.

voters as $ vote) {
            if ($ vote-> canVote ($ permission, $ subject)) {
                $ vote = $ vote-> vote ($ user, $ permission, $ subject);
                if ($ this-> debugger) {
                    $ this-> debugger-> debug ($ vote, $ vote, $ permission, $ user, $ subject);
                }
                if ($ vote === true) {
                    return true;
                }
            }
        }
        return false;
    }

    public function addVoter: void
    {
        $ this-> voters () = $ vote;
    }

}

This strategy offers the advantage of allowing the definition of transverse permission very simply

class AdminVoter extends Voting
{

    public function canVote (string $ permission, $ subject = null): bool
    {
        return true;
    }

    / **
     * @inheritDoc
     * /
    public function vote (User $ user, string $ permission, $ subject = null): bool
    {
        return $ user-> getRole () === Roles :: ADMIN;
    }
}

Interface detection can also be used to define general permission logic.

class OwnerVoter extends Voting
{

    public function canVote (string $ permission, $ subject = null): bool
    {
        return $ subject instanceOf Ownable;
    }

    / **
     * @inheritDoc
     * /
    public function vote (User $ user, string $ permission, $ subject = null): bool
    {
        return $ user-> getId () === $ subject-> getOwner () -> getId ();
    }
}

It is also necessary to adapt this solution to combine it with another approach (for example a database ACL permission system).

This system is therefore interesting because it can serve as a solid basis for defining permissions with different policies. On the other hand, it can sometimes be difficult to understand why a permission was given or refused. So do not hesitate to add a debugging tool to this system which will allow you to understand why a permission has been granted (or not) by indicating the Vote who participated and the results of their vote.