This class is responsible for ACL checks against classes and content objects.

This implementation is based on the general idea outlined in mRFC 15 ( http://www.midgard-project.org/development/mrfc/0015/ ), MidCOM Authentication and Access Control service. Developers Note: Be aware that the basic requirements for the ACL system undergone a major chance during the implementation, as the DBA layer with full access control even for database I/O was added. The proposals from the mRFC are largely outdated therefore. What is documented on the main MidCOM documentation has to take priority obviously.

Privilege definition

Privileges are represented by the class midcom_core_privilege and basically consist of three parts: Name, Assignee and Value:

The privilege name is a unique identifier for the privilege. The mRFC 15 defines the syntax to be $component:$name, where $component is either the name of the component or one of 'midgard' or 'midcom' for core privileges. Valid privilege names are for example 'net.nehmer.static:do_something' or 'midgard:update'.

The assignee is the entity to which the privilege applies, this can be one of several things, depending on where the privilege is taken into effect, I'll explain this below in more detail:

On content objects (generally every object in the system used during 'normal operation'):

  • A Midgard User encapsulated by a midcom_core_user object.
  • A Midgard Group encapsulated by a midcom_core_group object or subtype thereof.
  • The magic assignee 'EVERYONE', which applies the privilege to every user unconditionally, even to unauthenticated users.
  • The magic assignee 'USERS', which applies to all authenticated users.
  • The magic assignee 'ANONYMOUS, which applies to all unauthenticated users.
  • The magic assignee 'OWNER', which applies for all object owners.

On users and groups during authentication (when building the basic privilege set for the user, which applies generally):

  • The magic string 'SELF', which denotes that the privilege is set for the user in general for every content object. SELF privileges may be restricted to a class by using the classname property available at both midcom_core_privilege and various DBA interface functions.

The value is one of MIDCOM_PRIVILEGE_ALLOW or MIDCOM_PRIVILEGE_DENY, which either grants or revokes a privilege. Be aware, that unsetting a privilege does not set it to MIDCOM_PRIVILEGE_DENY, but clears the entry completely, which means that the privilege value inherited from the parents is now in effect.

How are privileges read and merged

First, you have to understand, that there are actually three distinct sources where a privilege comes from: The systemwide defaults, the currently authenticated user and the content object which is being operated on. We'll look into this distinction first, before we get on to the order in which they are merged.

Systemwide default privileges

This is analogous to the MidCOM default configuration, they are taken into account globally to each and every check whether a privilege is granted. Whenever a privilege is defined, there is also a default value (either ALLOW or DENY) assigned to it. They serve as a basis for all privilege sets and ensure that there is a value set for all privileges.

These defaults are defined by the MidCOM core and the components respectively and are very restrictive, basically granting read-only access to all non sensitive information.

Currently, there is no way to influence these privileges unless you are a developer and writing new components.

Class specific, systemwide default privileges (for magic assignees only)

Often you want to have a number of default privileges for certain classes in general. For regular users/groups you can easily assign them to the corresponding users/groups, there is one special case which cannot be covered there at this time: You cannot set defaults applicable for the magic assignees EVERYONE, USERS and ANONYMOUS. This is normally only of interest for component authors, which want to have some special privileges assigned for their objects, where the global defaults do no longer suffice.

These privileges are queried using a static callback of the DBA classes in question, see the following example:

public static function get_class_magic_default_privileges()
{
    return Array (
        'EVERYONE' => Array(),
        'ANONYMOUS' => Array(),
        'USERS' => Array('midcom:create' => MIDCOM_PRIVILEGE_ALLOW)
    );
}

See also the documentation of the $_default_magic_class_privileges member for further details.

User / Group specific privileges

This kind of privileges are rights, assigned directly to a user. Similar to the systemwide defaults, they too apply to any operation done by the user / group respectively throughout the system. The magic assignee SELF is used to denote such privileges, which can obviously only be assigned to users or groups. These privileges are loaded at the time of user authentication only.

You should use these privileges carefully, due to their global nature. If you assign the privilege midgard:delete to a user, this means that the user can now delete all objects he can read, unless there are again restricting privileges set to content objects.

To be more flexible in the control over the top level objects, you may add a classname which restricts the validity of the privilege to a class and all of its descendants.

Content object privileges

This is the kind of privilege that will be used most often. They are associated with any content object in the system, and are read on every access to a content object. As you can see in the introduction, you have the most flexibility here.

The basic idea is that you can assign privileges based on the combination of users/groups and content objects. In other words, you can say the user x has the privilege midgard:update for this object (and its descendants) only. This works with groups as well.

The possible assignees here are either a user, a group or one of the magic assignees EVERYONE, USERS or ANONYMOUS, as outlined above.

Be aware, that persons and groups are treted as content objects when loaded from the database in a tool like org.openpsa.user, as the groups are not used for authentication but for regular site operation there. Therefore, the SELF privileges mentioned above are not taken into account when determining the content object privileges!

Privilege merging

This is where we get to the guts of privilege system, as this is not trivial (but nevertheless straight-forward I hope). The general idea is based on the scope of object a privilege applies:

System default privileges obviously have the largest scope, they apply to everyone. The next smaller scope are privileges which are assigned to groups in general, followed by privileges assigned directly to a user.

From this point on, the privileges of the content objects are next in line, starting at the top-level objects again (for example a root topic). The smallest scope finally then has the object that is being accessed itself.

Let us visualize this a bit:

^ larger scope     System default privileges
|                  Class specific magic assignee default privileges
|                  Root Midgard group
|                  ... more parent Midgard groups ...
|                  Direct Midgard group membership
|                  User
|                  SELF privileges limited to a class
|                  Root content object
|                  ... more parent objects ...
v smaller scope    Accessed content object

Privileges assigned to a specific user always override owner privileges; owner privileges are calculated on a per-content-object bases, and are merged just before the final user privileges are merged into the privilege set. It is of no importance from where you get ownership at that point.

Implementation notes: Internally, MidCOM separates the "user privilege set" which is everything down to the line User above, and the content object privileges, which constitutes the rest. This separation has been done for performance reasons, as the user's privileges are loaded immediately upon authentication of the user, and the privileges of the actual content objects are merged into this set then. Normally, this should be of no importance for ACL users, but it explains the more complex graph in the original mRFC.

Predefined Privileges

The MidCOM core defines a set of core privileges, which fall in two categories:

Midgard Core Privileges

These privileges are part of the MidCOM Database Abstraction layer (MidCOM DBA) and have been originally proposed by me in a mail to the Midgard developers list. Unless otherwise noted, all privileges are denied by default and no difference between owner and normal default privileges is made.

  • midgard:read controls read access to the object, if denied, you cannot load the object from the database. This privilege is granted by default.
  • midgard:update controls updating of objects. Be aware that you need to be able to read the object before updating it, it is granted by default only for owners.
  • midgard:delete controls deletion of objects. Be aware that you need to be able to read the object before updating it, it is granted by default only for owners.
  • midgard:create allows you to create new content objects as children on whatever content object that you have the create privilege for. This means that you can create an article if and only if you have create permission for either the parent article (if you create a so-called 'reply article') or the parent topic, it is granted by default only for owners.
  • midgard:parameters allows the manipulation of parameters on the current object if and only if the user also has the midgard:update privilege on the object. This privileges is granted by default and covers the full set of parameter operations (create, update and delete).
  • midgard:attachments is analogous to midgard:parameters but covers attachments instead and is also granted by default.
  • midgard:autoserve_attachment controls whether an attachment may be autoserved using the midcom-serveattachmentguid handler. This is granted by default, allowing every attachment to be served using the default URL methods. Denying this right allows component authors to build more sophisticated access control restrictions to attachments.
  • midgard:privileges allows the user to change the permissions on the objects they are granted for. You also need midgard:update and midgard:parameters to properly execute these operations.
  • midgard:owner indicates that the user who has this privilege set is an owner of the given content object.

MidCOM Core Privileges

  • midcom:approve grants the user the right to approve or unapprove objects.
  • midcom:component_config grants the user access to configuration management system, it is granted by default only for owners.
  • midcom:isonline is needed to see the online state of another user. It is not granted by default.

Assigning Privileges

You assign privileges by using the DBA set_privilege method, whose static implementation can be found in midcom_baseclasses_core_dbobject::set_privilege(). Here is a quick overview over that API, which naturally only works on MidCOM DBA level objects:

  • TODO: Update with the set/unset call variants
  • TODO: Add get_privilege
Array get_privileges();
midcom_core_privilege create_new_privilege_object($name, $assignee = null, $value = MIDCOM_PRIVILEGE_ALLOW)
boolean set_privilege(midcom_core_privilege $privilege);
boolean unset_all_privileges();
boolean unset_privilege(midcom_core_privilege $privilege);

These calls operate only on the privileges of the given object. They do not do any merging whatsoever, this is the job of the CAL framework itself (midcom_services_auth_acl).

Unsetting a privilege does not deny it, but clears the privilege specification on the current object and sets it to INHERIT internally. As you might have guessed, if you want to clear all privileges on a given object, call unset_all_privileges() on the DBA object in question.

See the documentation of the DBA layer for more information on these five calls.

package midcom.services

 Methods

__construct ($auth)

Simple, currently empty default constructor.

Parameters

$auth

can_do_byclass ($privilege, $user, $class, $component)

Parameters

$privilege

$user

$class

$component

can_do_byguid (string $privilege, string $object_guid, string $object_class, string $user_id)

Checks whether a user has a certain privilege on the given (via guid and class) content object.

Works on the currently authenticated user by default, but can take another user as an optional argument.

Parameters

$privilege

stringThe privilege to check for

$object_guid

stringA Midgard GUID pointing to an object

$object_class

stringClass of the object in question

$user_id

stringThe user against which to check the privilege, defaults to the currently authenticated user. You may specify "EVERYONE" instead of an object to check what an anonymous user can do.

Returns

booleanTrue if the privilege has been granted, false otherwise.

get_default_privileges ()

Returns the system-wide basic privilege set.

Returns

ArrayPrivilege Name / Value map.

get_owner_default_privileges ()

Returns the system-wide basic owner privilege set.

Returns

ArrayPrivilege Name / Value map.

get_privileges_by_guid (string $object_guid, string $object_class, string $user_id)

Returns a full listing of all currently known privileges for a certain object/user combination.

The information is cached per object-guid during runtime, so that repeated checks to the same object do not cause repeating checks. Be aware that this means, that new privileges set are not guaranteed to take effect until the next request.

Parameters

$object_guid

stringA Midgard Content Object's GUID

$object_class

stringA Midgard Content Object's class

$user_id

stringThe user against which to check the privilege, defaults to the currently authenticated user. You may specify "EVERYONE" instead of an object to check what an anonymous user can do.

Returns

ArrayAssociative listing of all privileges and their value.

get_user_id (mixed $user)

Determine the user identifier for accessing the privilege cache.

This is the passed user's identifier with the current user and anonymous as fallback

Parameters

$user

mixedThe user to check for as string or object.

Returns

stringThe identifier

privilege_exists (string $name)

This is a simple helper function which validates whether a given privilege exists by its name.

Essentially this checks if a corresponding default privilege has been registered in the system.

todo This call should load the component associated to the privilege on demand.

Parameters

$name

stringThe name of the privilege to check.

Returns

booleanIndicating whether the privilege does exist.

register_default_privileges (array $privileges)

Merges a new set of default privileges into the current set.

Existing keys will be silently overwritten.

This is usually only called by the framework startup and the component loader.

If only a single default value is set (type integer), then this value is taken for the default and the owner privilege is unset (meaning INHERIT). If two values (type array of integers) is set, the first privilege value is used for default, the second one for the owner privilege set.

Parameters

$privileges

arrayAn associative privilege_name => default_values listing.

_can_do_internal_sudo (string $privilege)

This internal helper checks if a privilege is available during internal sudo mode, as outlined in the corresponding variable.
see \$_internal_sudo

Parameters

$privilege

stringThe privilege to check for

Returns

booleanTrue if the privilege has been granted, false otherwise.

_collect_content_privileges (string $guid, string $user_id, string $class)

Collects all privileges applicable for a user over a content object.
Static

It takes the complete content object tree (using the get_parent methods) into account and merges the privileges according to mRFC 15. It will only look at privileges that are applicable to the current user, with user privileges taking precedence over group privileges.

Known Issue: Currently, group privileges are merged in no particular order, in reality they should at least take the group tree into account when merging. They do take the "depth" into account correctly though when looking at a single group chain.

This function tries to operate on GUIDs whenever possible, to keep runtime up, only in the case of nonpersistent objects (no valid GUID yet), it will revert to regular object usage.

Parameters

$guid

stringThe GUID for which we should load privileges.

$user_id

stringThe MidCOM user assignee for which we should collect the privileges.

$class

stringThe DBA classname

Returns

ArrayAn array of privilege_name => privilege_value pairs valid for the given user.

_get_user_per_class_privileges ($classname, $user)

Parameters

$classname

$user

_load_class_magic_privileges (string $class)

This helper function loads and prepares the list of class magic privileges for usage.

It will assign them to the $_*_default_class_privileges members.

Parameters

$class

stringThe class name for which defaults should be loaded.

_load_content_privilege (string $privilegename, string $guid, string $class, string $user_id)

Look up a specific content privilege and cache the result.
Static

This implements the same semantics as _collect_content_privileges, but only works for one privilege at a time, which allows us to cut a few corners and therefore be faster

Parameters

$privilegename

stringThe privilege to check for

$guid

stringA Midgard GUID pointing to an object

$class

stringDBA Class of the object in question

$user_id

stringThe user against which to check the privilege, defaults to the currently authenticated user.

Returns

booleanTrue when privilege was found, otherwise false

_load_privileges_byguid (string $object_guid, string $object_class, string $user_id)

Returns a full listing of all currently known privileges for a certain object/user combination (object is specified by guid/class combination)

The information is cached per object-guid during runtime, so that repeated checks to the same object do not cause repeating checks. Be aware that this means, that new privileges set are not guaranteed to take effect until the next request.

Parameters

$object_guid

stringA Midgard GUID pointing to an object

$object_class

stringDBA Class of the object in question

$user_id

stringThe user against which to check the privilege, defaults to the currently authenticated user. You may specify "EVERYONE" instead of an object to check what an anonymous user can do.

_register_core_privileges ()

This internal helper will initialize the default privileges array with all core privileges currently defined.

 Properties

 

mixed $auth

 

Array $_content_privileges_cache

Internal cache of the content privileges of users on content objects, this is an associative array using a combination of the user identifier and the object's guid as index.

The privileges for the anonymous user use the magic EVERYONE as user identifier.

This must not be merged with the class-wide privileges_cache, because otherwise class_default_privileges for child objects might be overridden by parent default privileges

 

array $_default_magic_class_privileges

This listing contains all magic privileges assigned to the existing classes.

It is a multi-level array, example entry:

'class_name' => Array
(
    'EVERYONE' => Array(),
    'ANONYMOUS' => Array(),
    'USERS' => Array
    (
        'midcom:create' => MIDCOM_PRIVILEGE_ALLOW,
        'midcom:update' => MIDCOM_PRIVILEGE_ALLOW
    ),
)
todo This should be cached, as it would require loading all components by default. The component manifest might help here too.
 

array $_default_privileges

Internal listing of all default privileges currently registered in the system.

This is a privilege name/value map.

 

boolean $_internal_sudo

This is an internal flag used to override all regular permission checks with a sort-of read-only privilege set.

While internal_sudo is enabled, the system automatically grants all privileges except midgard:create, midgard:update, midgard:delete and midgard:privileges, which will always be denied. These checks go after the basic checks for not authenticated users or admin level users.

 

array $_owner_default_privileges

Internal listing of all default owner privileges currently registered in the system.

All privileges not set in this list will be inherited. This is a privilege name/value map.

 

Array $_privileges_cache

Internal cache of the effective privileges of users on content objects, this is an associative array using a combination of the user identifier and the object's guid as index.

The privileges for the anonymous user use the magic EVERYONE as user identifier.