Using Access Control
Perhaps the most complicated part of Access Control, which is the most difficult to understand from the outset is the actual boots on the ground usage of the rules to block users from accessing specific data. At its most abstract, this requires a couple of things: You must only allow them to view/edit/delete resource items for which they have the proper access to do so, and you must pass their credentials for each individual item to the client side (so that it can also know which permissions they have). But when you break down exactly what is done at every step, it can be a little more complicated than just this, so for this reason, we are going to first describe the different classes in Access Control that you use .
What are the different classes of Access Control to be in the serverside code?
There are two main classes that you will primarily interact with in order to properly control access to various items within your code are the classes of Engine
and Ruleset
. The class Model
is also used in a limited capacity, so it also deserves to be introduced. To understand at a conceptual level what each of these do, I will give a brief introduction.
The Access Control Engine
Before we get into the minute details of how to set up, and properly use these classes, an introduction is warranted. The Access Control Engine, or the Engine
class is designed primarily to setup and contstruct the Ruleset
for your specific user. The Access Control Engine is used when you are first initializing Access Control in both the Controller and the Model. It is the first thing that is done in order to be able to use Access Control. However, after you have initialized the Access Control for your Controller or Model (using the only function available publicly of getAccessRules
), you are returned an instance of the Ruleset
class, and you are dealing with that class from there on out.
The getAccessRules
function accepts two string parameters: the first required parameter is the user id, and the second optional parameter is the id of the current department the user is in. Best practice, however, would encourage you to always put in the current department id as well as the user id because the user is logged into a specific department at all times in our app. If, however, you choose to leave off the department id, it would return a Ruleset
for all Departments the user is in (any that that individual user or given to a group in any department that user is in).
There are of course, other protected functions, but these are not ones that you have access to, and are used internally to Engine
, so there is no need to discuss them.
The Ruleset
The Ruleset
class is obviously a class containing a Ruleset, which is defined in definitions. But as a reminder, a Ruleset is a group of rules and permissions for a specific user, which aggregates all their permissions from groups and departments they are in. The Ruleset
class, then, controls all access to that Ruleset, allowing you to do three main functions:
getResource
this function gets all the IDs that the current user has access to from a specific resource. It returns an array of resource IDs. It takes two parameters: the resource, which is required, and the Boolean Type, which is optional. Of course, it a type is defined, it will only return IDs that user has access to according to that type, but if none is provided it will provide a list of all ids that a person has at least base access to. Most often, this will be used in a browse request (since you need all ids), so you will leave the Boolean Type null. But, the option is there in case there is a use case of needing every single ID with X Boolean Type permission.checkResource
this function checks if the user has access to the given resource ID. It returns a Boolean as to whether the user does have access to that id. It takes three parameters: the first is the id of the item you are checking, the second is the string identifying the resource it is part of, and the final parameter is the Boolean Type you are looking for (this is optional). With this final parameter, you may pass in the booleantrue
to mean the Boolean Typeedit
(for backwards compatibility), or you may pass in the stringedit
directly if you wish not to use a boolean. Obviously, this function will check that specific resource either based upon base permissions (if no Boolean Type is passed) or based upon the Boolean Type given if one is.addRule
this function adds a rule to the cached ruleset. This is only needed if there is more than one ruleset within a single request, and the ruleset gets updated between them. It takes the same three parameters as checkResource: id of the resource, the name of the resource, and the Boolean Type of access it receives.
There are of course, protected functions, but these are not ones that you have access to, and are used internally to Ruleset
, so there is no need to discuss them.
The Model
The Model
class is primarily used internally within both Access Control and ember-fw-acl
, but there is one function which is vital to know how to use within Model when you are adding a new resource, and that is add
. The add
function is how you make a new rule, which must be called directly from the add
The add
function takes a variety of parameters:
- User Type (whether this rule is for
user
,group
, ordept
), - User Type ID (the user, group, or department ID)
- The resource name string
- The resource ID
- Permission Types, array (all from the Boolean Types that have been set up)
- Dept String for User Type, which is optional (if you want a "global" user in Access Control, who sees it from every department, leave this blank, but if it is defined to current department, it will only allow the user to see that in the current department).
While there are many other aspects to the Model
class, this is the only aspect that is relevant to actually setting up and using Access Control, so the rest of the information about the Model
class will be discussed in the Miscellaneous - Model section.
How to use these classes?
Now that we know how to use these classes conceptually, most Access Control implementations on the Serverside look borderline identical. So the rest of this page will walk you through how to implement these different classes within a Controller and a Model for a resource you are seeking to implement it.
In the Controller
There are many aspects of the controller, and this will change in some ways depending upon how granular your permissions get, and whether or not you choose to control a child Controller/Model upon the basis of the access control of the parent. But this will show you generally what you need to do to properly set up a Controller.
Setup and __construct function
Whenever you are constructing a Controller which uses Access Control, you always need to set up the Access Ruleset for that controller, and this is done using the built in __construct
function, and using the Engine
class. It is fairly simple, all you need to do is setup a protected property acRules
, and dump the contents of getAccessRules
from the Engine
class into them. In order to do this, you will need to inject the access-control.engine
, but it should look basically identical to this:
protected $acRules;
public function __construct()
{
parent::__construct();
if ($this->user->isAuthenticated()) {
$this->acRules = $this->inject('access-control.engine')
->getAccessRules($this->user->get('id'), $this->user->get('currentDept'));
}
}
If you notice, you are filtering for both the user and the department, and you are only adding Access Rules if the user is authenticated because otherwise, you will have problems getting everything to work right. Note: this step is needed for any Controller that you are planning to use Access Control in, whether or not it is the Controller that matches that resource name or not. For example, if there is a sub-model that is controlled by the access control for a parent's resource (for example Entries
are controlled by Services
in sstat or Threads
are controlled by Channels
in msgc), you will need to do this __construct
setup in every Controller that needs it (both the child and the parent).
The only other thing for setup, which is often done, though not always, is importing the ACModel
class. Here is how you would do it:
use AccessControl\Model as ACModel;
This step is only required in the Controller for the parent class, not the child, because it is only needed if you need to add an Access Control Rule into the database for a newly created resource. In the examples suggested above, you would only need to use this import if you are in the Channels
or Services
Controller, but not for the Threads
or Entries
.
The Browse Function
The browse
function is pretty simple, in that all you need to do is make sure that when you call findAll
, that you do so only with the ids you have access to, using getResource
. As an example, this is what that would look like:
$allowedTemplates = $this->acRules->getResource('template');
if (!$allowedTemplates) {
return $this->view->helper('json')->add([], 'templates');
}
$templates = $this->adapter->findAll('Template', ['id' => $allowedTemplates, 'dept' => $dept]);
This is how you would do it at its simplest. But, of course, if you are working with a more complicated browse with potentially a query, the $allowedTemplates
would need to be added to the query properly. Perhaps the most complicated is when you are actually passed in an $options->ids
array of ids, which you are seeking to find by, then you will need to use php's native array_intersect
in order to make sure that you are properly accounting both for the filter sent from the client and the only ones that that user properly has access to.
Most of the time, however, there are not such added complexities to deal with when looking at the browse function. In these more simple cases, the browse function will be just as simple as was listed above.
The Add Function
The add
function has some different complexities because you need to make sure that any item that you actually add has SOMEONE with permissions to edit it, otherwise, it will be lost in the database and no one will ever be able to see it. That being said, it is vital to use the ACModel::add()
, which was described as being imported properly above (in setup). Someone not only needs to be able to view this item, but be able to have full permissions to do anything that they need to. Now, this may vary depending upon the app, but most of the time, this is merely set to the current user to have all permissions, globally (leaving out the last department parameter). This makes it easier for the user to properly setup the permissions throughout the rest of the department. In certain cases, this could be set to the whole current department, but this is not advisable, and it should be only in very specific use cases that that was the chosen option. Here is an example of what this should look like, after the model has been added, using the adapter
call:
//get all Boolean Types from the config
$perms = $this->inject('access-control.config')->getBooleanTypes();
//initialize ACModel imported above
ACModel::init();
// In this case, there were Boolean Types which were used for another resource, so there was a need
// to filter the permissions to not include those irrelevant Boolean Types
// In cases where there is only one resource, or all resources share the same Boolean Types,
// this filter would be unnecessary
$perms = array_filter($perms, function($perm) {
return !in_array($perm, ['channelEdit', 'channelDelete', 'postThread', 'editThread',
'editAllThread', 'deleteThread', 'deleteAllThread', 'unpinThread', 'unpinAllThread',
'manageNotifications']);
});
//add the proper User rule to database
//Note that the final department string (for curDept) is left empty, thus creating a global user rule
ACModel::add('user', $this->user->get('id'), 'template', $template->get('id'), $perms);
// update rule cache, in case it needs to be checked later this network cache
foreach ($perms as $perm) {
$this->acRules->addRule($template->get('id'), 'template', $perm);
}
Now, if there was only one Boolean Type, being edit
without any more Boolean Types specially defined than that, this could be simplified to the following:
//initialize ACModel imported above
ACModel::init();
//add the proper User rule to database
//Note that the final department string (for curDept) is left empty, thus creating a global user rule
ACModel::add('user', $this->user->get('id'), 'template', $template->get('id'), ['edit']);
// update rule cache, in case it needs to be checked later this network cache
$this->acRules->addRule($template->get('id'), 'template', 'edit');
Read, Edit, Delete, and Other Functions
Most other times that you will be interacting with the acRules
will be using the checkResource
function. You will need to think of all the areas you will need to access this based on the specific permissions you have. At the least, you will need to add this check to read
, edit
, and delete
functions of BREAD, but it is entirely possible that there are other things controlled by Access Control. Here are some examples of other times you might desire to use this checkResource
:
- If there is any control of any child model, which is controlled, you will need to use
checkResource
on all of their BREAD functions (ie, is there a specific rule for adding an entry to a service, as opposed to deleting an entry from a service). Unlike browse and add for the resource Controller itself, a child Controller will requirecheckResource
to check that it has access to the selected resource (to add or even to browse) before allowing the user to continue with that action. As granular as you decide to go with permissions, will require you to go to a deeper level. - Sometimes there may be additional actions within that specific Controller (for the resource itself) which is controlled, and needs to use a
checkResource
before allowing the action to move forward. Potentially, this could include things, such as sending an email, managing notifications for that service, etc. - Depending upon how granular the resource goes with Boolean Types, you may need to call
checkResource
multiple times within the same function. As a hypothetical example, maybe there is a specific field that is controlled by a different permission than the general edit. In this case, after checking that the user has access for the general edit, you would need to check the more specific permission and if they don't have it, unset that property.
So critical thinking is required to think about how you will be using Access Control in your specific app, but generally, read, edit, and delete will look very similar. Here is an example of how you would need to use checkResource
in each of those cases, and hopefully, this will help you to determine how to use it if you have other permissions which need it:
//READ function
//In this example, we are merely checking if they have base view access to the template
$hasPermission = $this->acRules->checkResource($options->id, 'template');
if (!$hasPermissions) {
//in this case it returns nothing if there is no permission
return $this->view->helper('json')->add([], 'template');
}
//EDIT function
// In this case, there is a special permission separate from normal edit function, which allows for
// editing of the resource
// In other cases, this may just be passing true or "edit" as the type if you didn't make a specific
// Boolean Type for this
$hasPermission = $this->acRules->checkResource($options->id, 'template', 'editTemplate');
if (!$hasPermission) {
//Notice, here you get a StatusException for attempting to edit without permission
throw new \StatusException("You do not have permission to edit this template!", 403);
}
//DELETE function
// Again, in this case there is a more granular permission for deleting a template that was set up as
// a Boolean Type
// In cases where there is not more specific Boolean Types, this would need to be checked against
// 'edit' (or true) as above because if a user cannot edit a resource, they certainly shouldn't
// be able to delete it
$hasPermission = $this->acRules->checkResource($options->id, 'template', 'deleteTemplate');
if (!$hasPermission) {
throw new \StatusException("You do not have permission to delete this template!", 403);
}
Again, the checkResource
is fairly self explanatory, and can probably be understood fairly easily, it just takes the most thought as to all the places you will need to put it based on your Boolean Types.
In the Model
In order to finish getting the serverside set up for Access Control, the last thing you need to do is to pass over the Boolean Types to the client, so that the client can properly render things (for example, sometimes certain buttons will only show if that user has a certain permission in Access Control for that ID). While a lot of this is done on the client side, and thus, you must reference the ember-fw-acl
documentation for information about that, we also still need to pass it to the client in the Server Model through the toJson
function.
The first step is to import CurrentUser
from Group Control into the Model, so that you can access the user ID and the current department. Additionally, you must import ContainerAwareTrait
, so you are able to properly call init
. These can be done with the following import statements:
use GroupControl\Utils\CurrentUser as User;
use FW\Utils\ContainerAwareTrait;
The second step is that with ContainerAwareTrait
, you also need to use it inside of the class itself. As an example, this would look like:
class Channel extends Data\Standard\Model {
use ContainerAwareTrait;
//...everything else in the model
}
The only other thing to do is to format the toJson
function to call the Access Control Engine to initiate the Ruleset, then to check the Ruleset for each Boolean Type to pass to the Model. Here is an example of how to setup the toJson
if you have custom Boolean Types (other than just edit), and you have multiple resources:
// determine user permissions if there are several different Boolean Types for various resources:
//initialize CurrentUser class, so you can access functions in it
$user = User::init();
//get your Ruleset set to rules, so you can check booleans
$rules = $this->inject('access-control.engine')->getAccessRules(
$user->get('id'), $user->get('currentDept')
);
//inject Access Control Config so that you are able to check each Boolean Type that is set up
$acConfig = $this->inject('access-control.config');
//get each Boolean Type to send to client
foreach ($acConfig->getBooleanTypes() as $bool) {
//in this case, we must ignore Boolean Types desiged for other models (such as Channel
// Permissions) because these are irrelevant to Template
//If all of the Boolean Types were connected to the same Resource, then we would not need the if
// statement, just the checkResource call.
if (!in_array($bool, ['channelEdit', 'postThread', 'channelDelete', 'editThread', 'editAllThread',
'deleteThread', 'deleteAllThread'])) {
$json[$bool] = $rules->checkResource($json['id'], 'template', $bool);
}
}
Here is an example of how to set up toJson
if you have only the edit
Boolean Type on this resource:
//determine user permissions if there are no granular Boolean Types, so that edit is the only Boolean
// Type initialize CurrentUser class, so you can access functions in it
$user = User::init();
//get your Ruleset set to editRules, so you can check booleans
$editRules = self::inject('access-control.engine')
->getAccessRules($user->get('id'), $user->get('currentDept'));
//Now all you have to check is edit, because that is the only Boolean Type that exists
$json['edit'] = $editRules->checkResource($json['id'], 'entry', true);
Note it is important to note that if you are adding a toJson
function to the Model file that you include the following structure for the toJson
function:
function toJson() {
$json = parent::toJson();
//... all content and change to $json object such as described above
return $json;
}
If you already have a toJson
function, this is already done for you.