doc/developer/UserRolesArchitecture

Original author: Benoit Grégoire, last modified: 2007-07-12

New user permission architecture, General model

Why our own rights and permissions framework

We couldn't find one that allowed tying rights to objects instances (and not classes). Otherwise,  LiveUser would have almost done the trick.

Stakeholders (Role and permission types)

Each role and permissions (rights) are relevant to one of the following object type or static object:

  • Node
  • Network
  • Server
  • Content

These define the stakeholder types.

Note: The right to access a specific Content type will be a network right. This way, we can define a "Beginer editor" and "Advanced editor" roles

Permissions

Permissions are defined in the code, all in a big PHP lookup table to allow easy translations. An example of permission would be NODE_PERM_EDIT_METADATA. Being a Node permission type, the user who get's this permission (a node stakeholder) gets to be allowed to edit the metadata of the specific node instance that this permission applies to. Permissions are defined in Permission::getPermissionArray().

$PERMISSIONSPERMISSION_ID? = array( _("permission description"), // The description of the permission

Stakeholder::Node, // The permission type

true // Wether or not all EXISTING roles of that type should have that permission granted.This is only used when syncing the permissions. It is NOT if logically all roles should have that permission by default. It is meant to maintain prior functionnality of a server untill the administrators can review the different roles. In general, when adding new permission types to restrict existing functionnality this should be set to true. When adding brand new functionnality, this should be set to whatever is logical as default behaviour.

);

Adding or removing permissions

  • You do NOT need to update the database schema when adding or removing permissions. The database is kept in sync by a script (Permission::syncPermissions()) that is run everytime an exception is caucht when instanciating a permission. This way, there is no performance cost, and the maintenance burden is considerably reduced: Just define your permission and use it, and the database relationships will automagically be updated.
  • Permission granularity: One of the main point of the role system is to have much more granular permission, and then manage this forest of potential permission type using the role system. While more granular permission won't be added overnight, when you do add more granular permission, PLEASE delete the old, larger permission.
  • Where to put permission checks: In general, in the interface methods of objects: getAdminUI(), processAdminUI() and delete(). Do not put them in setter or getters, as other parts of the code may need to use them.

Roles

Roles are administrator-defined groups of permissions of the same type as the role (Node, Network, etc.). For example, if we define the "Tech support" Node role, we can assign various Node permissions to this role (but only node permissions).

If we then assign a user the "Tech support" role for a specific node, that user becomes a "Tech support stakeholder" for that node.

Node (ex: Node tech support, Node owner, etc.)

Network (ex: User allowed to login multiple times, Content curator (can edit other people content(, Content editor, etc.) Server Content

User types (system roles) (NOT YET IMPLEMENTED)

While administrators can define new roles, there is a need for a few SYSTEM defined roles that are ALWAYS present.

  • Anyone (anonymous)
    • maybe just special-case an ID, like SPLASH_ONLY_USER?
    • Exists for server, network, node, etc.
  • Logged-in user
  • Validated users
  • Users in validation

User validity levels (Allowed, Validation, etc.) will map to system roles, and may eventually be deprecated.

Groups

To keep things simple, there is no concept of groups in the classic sense. Roles allow doing much the same thing, and are simpler to implement.

Implicit permissions

To keep things simple, defining implicit permissions (such as making NETWORK_EDIT_ANY_NODE imply NODE_PERM_EDIT_METADATA for each node) will not be supported. While this is a desirable feature, implementing a simple UI for this is not easy, and since most permissions apply to specific objects, the code to check them would be complex and have low performance.

This means that when using permissions in the code, BOTH will have to be specified as allowed. The API will be written to make this easy.

Note that once again, properly defined roles do much the same thing. (For example, a node owner can have a certain number of implicit node permissions).

Sudo support

It would be extremely useful to allow an administrator to masquerade as another user for testing and tech support purposes. Logging out would then get him back to his original user. As user handling is centralized, this should be reasonably easy to implement.

Handling of insufficient permission

In the common case, missing permissions should be handled through exceptions (using the Security::require*() methods). This will solve two annoying problems, while getting rid of just about all current permission error handling code:

  • If the user is not logged in (because of a timeout, or never logged-in), it allows him to login again.
  • If the user doesn't have the necessary permission(s), inform him (telling which permission is missing) and allow to login as another user.

In both cases, if the user successfully logs-in (with the rights permission), the get and post parameters are regenerated and the original operation is attempted again.

Exception Implementation

There is now a global exception handler defined in MainUI.php outside MainUI. When an exception is thrown, it will call a new MainUI with the prettily formatted error message and the request-repetition code. Now it's really easy to not get stuck in the middle of a half-rendered page, and we don't have to wrap all the top level scripts in try-catch blocks.

API

To most developers, only two classes matter: Permission (to define permissions) and Security (to use them)

These function are to be prefered in most cases for permission checks in processAdminUI, as they will throw exceptions to allow the user to login (or login as another user) to retry the operation:

Security::requirePermission(Permission $permission, $target_object, $user=null);  
Security::requireAnyPermissions(Array $permissionsArray, $target_object, $user=null);
Security::requireAllPermissions(Array $permissionsArray, $user=null);

Mirror functions exist to simply check if the user has the permissions. Generally used in displayAdminUI (if specific options are unavailable, but the user should still be able to edit other parts of the object), when building menu, etc.

Security::hasPermission(Permission $permission, $target_object, $user=null);
Security::hasAnyPermissions(Array $permissionsArray, $target_object, $user=null);
Security::hasAllPermissions(Array $permissionsArray, $user=null);

Internal implementation, and corner cases (gateway interaction maybe)

Security::getObjectsWithPermission(Permission $permission, $user=null);  //TO find an object on which the user has the permissions.  Especially usefull to build lists in menus and select boxes.
Security::hasRole($role, $targetObject, $user);  //user is optional, if unspecified, the current user is used.  User can also be null, meaning that there is no user currently logged-in

Mostly for reporting (NOT YET IMPLEMENTED)

Security::getUsersWithPermission($permission, $targetObject); //Return list of users with this permission
Security::getUsersWithRole($role, $objectId); //Return an array of users with the given role.  If objects_id is null, all users with the specific role are returned, along with an array of objects for which they have this role.  Maybe this function won't actually be implemented, as it's there mostly for reporting and sending notification of hotspots going down.
Security::getPermissions($user);  //returns array of PERMISSION constants for this user.

Data model

stakeholder_type table

  • stakeholder_type_id

permission table

  • permission_id REFERENCES permission_type
  • stakeholder_type_id REFERENCES stakeholder_types

roles table

  • role_id text NOT NULL,
  • role_description_content_id text,
  • is_system_role bool NOT NULL DEFAULT false,
  • stakeholder_type_id text NOT NULL REFERENCES stakeholder_types,
  • role_creation_date

role_has_permissions

  • role_id REFERENCES roles
  • permission_id REFERENCES permissions

One table per stakeholder type, that all inherit from the stakeholders table:

stakeholders

  • user_id text NOT NULL
  • role_id text NOT NULL
  • object_id text NOT NULL

The tables are named from the stakeholder types. For example, for nodes, the table is called node_stakeholders:

Note that the stakeholder tables list user-role pairs (and NOT user-permission pairs)

Problems to be solved by this

Example of things this will allow (this is only the list from Max Horvath, there are many more things waiting for this).

  • Allow owners to have more granular permission to edit content (some can edit login, some not, etc.)
  • Restrict user access to a single node (user_can_access_all_nodes, netowork permission)
  • Fon-like: user can login only if his node is online
  • Time limited internet usage

TODO: Discuss where this fits into auto-node creation