- New user permission architecture, General model
- Handling of insufficient permission
- Data model
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:
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 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 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.
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.
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).
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.
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.
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.
- permission_id REFERENCES permission_type
- stakeholder_type_id REFERENCES stakeholder_types
- 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_id REFERENCES roles
- permission_id REFERENCES permissions
One table per stakeholder type, that all inherit from the stakeholders table:
- 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