Authentication & Authorization

When you create a normal website, you usually have to consider how users will login and how you will securely store that information. Fortunately, when creating a module, you don’t need to think about any of that! Anyone using your module will be logged in.

Types of Users

There are two different types of users in the portal, database users and control users. A database user is generally used to log in. It may have a password and email verification assigned to it, and possibly social media provider IDs. The control user is the actual user, which contains information about the user and is in groups and roles.

We use these two users to abstract the login process away from control. Control can be purely focussed on the user, and therefore can be substituted for any other user management system. The database users will stay the same and allow for the same login process no matter where the user data is stored.

Database User

You generally won’t need to access the database user, since nearly everything a module does will be through the control user. If you ever find yourself needing to though, you can resolve the \BristolSU\Support\User\Contracts\UserAuthentication contract from the container.

$userAuthentication = app(\BristolSU\Support\User\Contracts\UserAuthentication::class);
$databaseUser = $userAuthentication->getUser();

This function returns null if no user is logged in, or an instance of \BristolSU\Support\User\User.

Control Models

These are generally the models you will work with. When accessing your module, you will always have access to the control user. In some cases, your module may also be used in the context of a group membership or a group role, meaning you’ll have access to the control group and the control role.

This may be sounding worrying in terms of how many bits of information you need to consider, but later on in this page we simplify this to make module development a lot easier. It is always worth bearing in mind though, especially if your module directly uses one of these models.

Retrieving the model

The \BristolSU\Support\Authentication\Contracts\Authentication class is the way to retrieve information about the current control user. This works on both the web and the API, as long as it is resolved out of the container. This means you can type hint it into your controller methods, or resolve it directly from the container.

$authentication = app(\BristolSU\Support\Authentication\Contracts\Authentication::class);

// Returns an instance of \BristolSU\ControlDB\Contracts\Models\User
$controlUser = $authentication->getUser();

// Returns an instance of \BristolSU\ControlDB\Contracts\Models\Group or null
$controlGroup = $authentication->getGroup();

// Returns an instance of \BristolSU\ControlDB\Contracts\Models\Role or null
$controlRole = $authentication->getRole();

Requiring a control model

Most of the time, a module will be able to work with any combination of control models. Whether or not a group/role is logged in shouldn’t matter to the module, generally it will only affect things like permissions and settings which are defined by the user when setting up a module.

Occasionally, however, a module will need to have a specific model. For example, we have a module which is used to assign roles to a group. Since this then depends on having a group in the module to assign the roles too, this module can only be used when a group is logged in.

To tell the portal this is the case, you can put a 'for' key in your config. This takes one of 'user', 'group' or 'role', and defaults to user. If it is user, then as long as a user is logged in the module can be used. If it is group, then both a user and group will be accessible to the module. If the module is defined as for a role, a user, group and role will always be accessible to the module. In this way, you can create modules based around control models without worrying that a given model won’t be availabl.

<?php

return [
    'name' => '...',
    'description' => '...',
    'for' => 'group' // Could also be 'role', defaults to 'user'
];

Referencing users, groups and roles

Having users, groups and roles adds a lot of flexibility to the way the portal works, but it also adds a significant amount of complexity in terms of referencing the user/group/role. In our module, we generally won’t care if just a user is logged in, or a user/group/role combination are, until it comes to saving data.

Say we’re creating an upload file module. If the module has been put in the context of a role by the admin of the portal, anyone in the same role should be able to access the same files. If, however, the module is in the context of a user then only the user should see the files. When saving a file in the database, we then need to reference not only the ID as normal, but also the type of the model. At the most basic level, this would just be the user ID, but this could be the Role ID and the model type role, or the same for a group.

The Activity Instance

The SDK aims to eliminate this added complexity and return to the idea of a single ID using the idea of an Activity Instance. An Activity Instance is a unified ID for any user/group/role. Modules no longer need to consider each of the user/group/role situations unless relevant to the module operation, and instead just think about the single ID Activity Instance ID.

When a user starts an activity in any context, they will create a new activity instance. By saving the file (or any other data) against the activity instance ID, and the module instance ID, you can ensure you always pass the correct information to the user. The activity instance ID contains information about the user/group/role accessing the data, and the module instance ID contains information about which data to retrieve.

Database Setup

Rather than using the normal user_id column in your module, you will need to have the following columns on any tables you may need to retrieve for a user. For example, we’d have this on a 'files' table, but not on a 'file comments' table since they are attached to a file.

$table->unsignedInteger('module_instance_id');
$table->unsignedInteger('activity_instance_id');

Accessing Data

To even further simplify the user system for you, we provide traits/scopes to help you carry out common queries. All you need to do is add the \BristolSU\Support\Authentication\HasResource trait to any eloquent model which uses the module instance and activity instance pattern.

When a new model is created, the module instance ID and activity instance ID are automatically resolved and saved if not given. When creating models for a user, this is good enough. When creating a new model as an admin, who themselves doesn’t have an activity instance ID, we will need them to select one to save the model against. In this case, we will have to pass the activity instance ID to the model on creation, and the trait will fill in the module instance ID for us.

To retrieve data for a specific user/group/role (activity instance), we can use the forResource($activityInstanceId = null, $moduleInstanceId = null) scope. This is used on the participant side, and will apply a where contraint to the query builder to only retrieve rows for the given activity instance and module instance. If no activity instance or module instance are given, it will use the current ones. Therefore, to retrieve all files for the current user, we’d use

$files = \My\Namespace\File::forResource()->get();

On the admin side, you generally will want to retrieve all data for the module instance, regardless of the corresponding activity instance. This is equivalent to showing all files from all users for the specific module instance in a file upload module, which is what would usually be expected on an admin side. For this, a forModuleInstance($moduleInstanceId = null) scope is provided. If the Module Instance ID is null or not given, it will search for all rows with the current module instance ID.

Using User IDs

Although this handles the basic retrieval of data, we will sometimes want to know who uploaded something as opposed to which resource it is for. If you have a file upload module in a group activity, the activity instance will be relevant for the group only. This means that we know the file belongs to Group 1, but nothing more about it.

We may want to know that Jane uploaded the file as opposed to Joe, which is information that will be lost during the upload as we essentially only save the group. Therefore, we strongly recommend always adding a user_id column to your tables and manually resolving the control user from Authentication and saving their ID in here.

Here is an example of the migrations and controllers for an Upload File module, where a file can be uploaded and retrieved by a user and viewed by an admin. If an admin needed to upload a file, they’d have to select the activity instance and this would need to be saved into the model manually as opposed to using the trait, as admins aren’t logged into activity instances by default.

Database Migration

Schema::create('uploadfile_files', function(Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->text('description')->nullable();
    ...
    $table->unsignedInteger('uploaded_by');
    $table->unsignedInteger('module_instance_id');
    $table->unsignedInteger('activity_instance_id');
    $table->timestamps();
    $table->softDeletes();
});

Participant Controller

class ParticipantFileController
{

    public function store(\BristolSU\Support\Authentication\Contracts\Authentication $authentication)
    {
        returm File::create([
            'title' => $request->get('title'),
            'description' => $request->get('description'),
            ...
            'uploaded_by' => $authentication->getUser()->id(), // Get the User ID
        ]);
    }

    public function index()
    {
        return File::forResource()->get();
    }

}

Admin Controller:

class AdminFileController
{

    public function store(\BristolSU\Support\Authentication\Contracts\Authentication $authentication)
    {
        returm File::create([
            'title' => $request->get('title'),
            'description' => $request->get('description'),
            ...
            'uploaded_by' => $authentication->getUser()->id(), // Get the admins User ID
            'activity_instance_id' => $request->get('activity_instance_id') // Must be passed through the API manually
        ]);
    }

    public function index()
    {
        return File::forModuleInstance()->get();
    }

}

Using these tools, we can now save and access data in the database in a way that works with the flexible user control system of the portal.

Permissions

Finally, we want to allow users of the portal to define who can do specific things within a module. Back to our example of a file upload module, some users may be able to upload a new file and others just see uploaded files.

The SDK defines a flexible permission framework for assigning permissions. All we need to do is let the SDK know what permissions your module needs, and check the permissions in the correct places.

What permissions to use

Although the permissions you will need will depend on your module, these generalisations will help to guide the permissions you need.

  • Each participant page should have a permission.

  • Each participant API route should have a permission.

  • Each admin page should have a permission.

  • Each admin API route should have a permission.

Registering permissions

Each permission consists of a key, which is a string. These must start with your module alias, but it’s up to you what you use after that. We recommend creating your permission from a string separated by full stops. If the permission is for an admin, put this in the second position. The third position could then be view-page or similar, or if you have a REST api the resource should go here with the action fourth. Here are some examples:

module-alias.admin.view-page // View admin page
module-alias.view-page // View page
module-alias.admin.file.download // Download for an admin
module-alias.file.download // Download for a participant

A permission also consists of a name and description.

There are two ways to register permissions with the SDK. The easiest one is to register each permission in the array in the service provider, and the other is to use the interface directly.

Service Provider

For each permission, you can use the $permissions array in your module service provider. The key should be the key for the permission as described above, and the content should be an array with a name, description and admin element (a boolean which specifies if the permission is for an admin or not).

protected $permissions = [
    'view-page' => [
      'name' => 'View Participant Page',
      'description' => 'View the main page of the module.',
      'admin' => false
    ]
];

You may notice your module alias isn’t at the start of the array key. The SDK automatically appends your module alias to the start.

Directly with the interface

You can also use the permission store interface directly.

\BristolSU\Support\Permissions\Facade\Permission::register('module-alias.permission-key', 'Permission Name', 'Permission Description', 'module-alias', $isAdmin);

Authorizing (Checking Permissions)

You now need to check that someone has a permission when they try and carry out an action. If the user doesn’t have a permission, we should return a 403 error to the user.

To do this, we recommend overriding the default 'authorize' function in your base controller to add your alias to the ability. This will allow you to refer to the permission as 'view-page' not 'module-alias.view-page'.

If you don’t want to do this, make sure to refer to the permission in its full form with your alias at the start.

class Controller
{
    use AuthorizesRequests {
        authorize as baseAuthorize;
    }

    use DispatchesJobs, ValidatesRequests;

    public function authorize($ability, $arguments = [])
    {
        return $this->baseAuthorize(
            'my-alias.' . $ability,
            $arguments
        );
    }
}

We can now authorize the user. In the participant page controller, we just have to put the following line at the start of the method to automatically check the user has the permissions, and throw an error if they don’t.

$this->authorize('view-page');

Of course, this is a general permission check to make sure a user can do something. There are additional checks that need to be made to secure your module. These checks revolve around particular models. For example, you need to check a model actually belongs to the correct module instance. You also may need to check a model belongs to the correct activity instance, or the user is allowed to perform an action on this model.

Checking Module Instance Access

Let’s take an example of a file. We need to check the file belongs to the module instance, so that it can only be accessed through a single page. If we have two different file upload modules, e.g. a constitution and a risk assessment, we only want to see consitutions in the consitution module and vice versa.

To do this, we tend to use route model binding. This has the additional benefit of ensuring our route model binding doesn’t clash with any other modules as long as we bind using our module instance

In your module service provider boot method, you can bind a model as follows.

Route::bind('uploadfile_file', function($id) {
    $file = File::findOrFail($id);
    if(request()->route('module_instance_slug') && (int) $file->module_instance_id === request()->route('module_instance_slug')->id()) {
        return $file;
    }
    throw (new \Illuminate\Database\Eloquent\ModelNotFoundException)->setModel(File::class);
});

We can then use {uploadfile_file} in our route as a parameter to require a file ID and have it injected into the controller. If the module instance ID of the file is different to the current module instance ID, we throw a 404 error.

Checking Activity Instance Access

This is the portals way of checking the user has access to the file. This should only really be done on the participant side, since the admins can access all activity instances , or more specifically all models, in the module instance. On the participant side, in the controller, we tend to put the following:

if((int) $file->activity_instance_id !== (int) app(\BristolSU\Support\ActivityInstance\Contracts\ActivityInstanceResolver::class)->getActivityInstance()->id) {
    throw new \Illuminate\Auth\Access\AuthorizationException;
}

This will return a 403 error if the file does not belong to the activity instance. Therefore, if we had a group activity, anyone in the group would be able to access the file but anyone outside the group wouldn’t be able to.

Checking for model ownership

This may only apply for a few of your routes. If you want to limit, for example, a files deletion to only the person who initially uploaded it (as opposed to the group or role that it was uploaded for), you can add an additional check against the current user control model, ensuring the ID is the same as the user id you have saved on the model. Throwing an AuthorizationException as above will take care of formatting the error for you.

Checking Permission Access

You won’t always want to throw an error if a permission is not owned though. For example, you may want to show a button if a user has the 'upload-file' permission. To check if the button should be shown, we can use the SDK PermissionTester. An example method call for checking if the currently authenticated user has the given permission would look like

\BristolSU\Support\Permissions\Facade\PermissionTester::evaluate('upload-file.view-page');

This will either return true or false depending on if the user has the permission or not. The SDK hijacks the Laravel permission framework and forces all permission tests through this method, meaning you can use any normal Laravel permission tool (e.g. @can('my-alias.upload-file') in blade templates) to check permissions. This permission checking will all be done on the currently authenticated user. To check a given user, use the evaluateFor method, which additionally accepts a user, group and/or role.

Finally, as a shortcut to using the PermissionTester, we have created a helper function hasPermission($ability (e.g. 'mymodule.view-file')) that can be called from anywhere, which will call the PermissionTester and return the result. Pass in just the ability to check the current user, or pass in a user/group/role to check the given user/group/role instead.