Open Source Content Management System

mRFC 0015: MidCOM Authentication and Access Control service

For quite some time now, MidCOM does not explicitly support any form of Authorization or Access Control. There are many cases of patchwork that check for various access restrictions during runtime, but they are not unified or centrally controlled in any way. This mRFC deals with this problem.

Recommended Reading:

Requirements: Access control

The main reason for this service is the fact, that all kinds of access control checks are implemented in slightly different variants again and again throughout MidCOM. This makes both maintaining and extending any kind of Permission system rather difficult. This is especially a problem, since MidCOM does implement a good deal of additional permission checks beyond the basic access control put in place by Midgard itself.

The service should decouple components from this as far as possible, to allow a later migration to MgdSchema without much hazzle. The API of this new interface is content oriented, so that integration into NAP is easily doable.

Access control has to be extended down to the object level, going further then the current node-level security. In addition, all kinds of objects must be supported, for example allowing even for shared event trees used by mulitple component instances. The natural obejct tree of Midgard must be kept in mind here.

Basically, a permission system can be separated into two parts, permission grants and permission checks, which must be supported by MidCOM. Also, the permissions come in different natures, content access permissions and operational permissions.

A new feature introduced with this mRFC is the notion of virtual groups, which provide a way for components to introduce new transparent groups (like all reservation members) without having to trace the corresponding Midgard Groups. They are not supported for write-access control, but are mainly intended for operational permissions.

Access Control Lists

The whole system will be built on an Access Control List system (ACL). An ACL consists of a set of permissions. Each permissions requires information about the type of operation involved (e.g. read or write access), a user or group for which the permission is applied (if applicable) and finally the value of the permission (grant, deny or inherit).

Permissions come from two sources: The user database and the content objects. Both have to respect the object hierarchy, giving a merging chain like this:

Parent Group(s)
|
v
Member Group(s) Parent Object(s)
| |
v v
Person Specific Content Object
+-----------+------------+
|
v
Final permission set
for a given content object

Note, that the parent chain of the content side is not neccessarily terminated in the MidCOM Topic tree. Instead, it follows the natural uplink chain for the objects in use. This allows for conent trees which can be shared between several topics easily. On the other hand, it will be possible to manually override the uplink tracking to "switch" it to a topic. Using this feature, you can "assign" arbitary objects to a content topic (this will be superseeded by MgdSchema driven content trees).

Permissions assigned to user/group directly naturally assign the permission generally for the user/group in question and thus don't need a user/group value in the permission set. Of course, such global permissions can be overridden in the content tree by explicitly denying the permission to a given group.

While building the permission set, permissions have to be granted explicitly. All permissions are denied by default, and if a given operation is undefined at any object, its value is inherited from the parent.

Virtual Groups

A virtual group is defined and supplied by a component, but treated like any other group when it comes down to acl operation (thus hiding the difference everywhere unless loading the actual group and membership information). Virtual groups are not bound to any specific topic and will therefore be bound to work in the core component interface. This information must be cached, otherwise access control will be quite slow.

Content access permissions

This set of permissions basically controls who can access a given content object, available access options are "Read access" and "Write access". Both can be queried using the regular ACL query system outlined below. They can only be defined in the content part of the tree. Read/Write permissions out of the user permissions are not taken into account when calculating accessibility.

Write access is implemented using the Midgard Ownership scheme. A user has write permission to a given content object if and only if he is an object owner. Write permissions are therefore set and checked using the regular Midgard operations keeping the semantics. Note, that MidCOM checks write-permission by default on a per-topic level (see above). If you use any objects outside of the topic tree, you need to ensure the correct Midgard level permissions yourself (both as site administrator and component author).

Read access cannot be controlled on a Midgard level, it is not supported there. Aegir originally introduced the concept of ViewerGroups to account for this problem. Originally, they were set on a per-topic basis, specifying a list of groups that have access to the given topic tree. For backwards compatibility, this general strategy is kept and the Parameter Domain ViewerGroups, is populated with compatibility values automatically. See Midgard Documentation, API Reference, Parameters, Registry of used Parameters. Also for backwards compatibility, if no readd access control is set, it is treated as if all users have read permissions.

Operational permissions

These permissions make up the successor to the old Aegir User Interface Parameters. Utilizing a new more flexible scheme this part is geared for extensibility.

Operational permissions are evaluated in the context of the active user, all groups he is associated with and the topic currently being handled. These contexts are merged in a lest-specific to most-specific strategy. For both groups and topics, the tree is taken into account too, with the parent objects counting as less specific then the child objects.

An operational permission is in its essence a boolean flag, either granting that permission, or denying it. All permissions default not granted. At each level of the inheritance tree, an operational permission may be granted or denied explicitly, overwriting the current value, or not specified at all, leaving the current state unchanged.

Operational permissions are grouped by the component they belong to and a unique identifier, separated by a colon. So the MidCOM PowerUser setting might be midcom:power_user, while the downloads component might restrict configuration using net.nemein.downloads:set_release. The component interface needs to be extended so that AIS will be able to query the available operational permissions for a given component; with this information present, AIS can automatically build permission management UIs.

When storing them, all operational permissions are stored within the parameter domain midcom.service.auth. The exact storage format proposal is outlined below.

Administrative users

All Midgard Administrators automatically receive complete and unlimited access to the complete site. Neither Content nor Operational access control is in place for them, all checks will always return "Permission granted" for them.

Automatic Permission checks

While parsing a request, the request processor will check read permissions on the topic tree as he "comes along". NAP will do the same when loading NAP information.

Explicit Permission checks

The new Permission check interface must provide a simple check method, which checks whether a given permissions is available. A second helper method might be used which automatically generates an error page if the permission is unavailable, keeping the code in the components at a minimum.

Compatibility with existing applications

As always, this one of the million-dollar questions. Aegir has two major features, which are in use in MidCOM: One of it is the Power User Flag, which is sort of a half-way admin, allowing a user to do more advanced stuff which still does not require admin level privileges. The other is the Viewer Groups Permissions, which restrict read access to topics to all groups not listed in the Viewer Groups collection.

The ViewerGroups implementation will be superseeded by an ACL solution, but will duplicate the group permissions into the old domain as long as it Aegir 1.x is in use.

The PowerUser permission should be migrated to the new Operational Permission system, for the sake of consistency (I don't like the thought that this single permission is handled differently then the rest).

Authentication Requirements

  • integrate nemein auth (or a successor) into the framework
  • have different auth mechanisms (at least basic, md5, cookie and component-manual auth)
  • allow a per-request selection of what authentication method to use

Along with Authorization always comes Authentication. Until now, MidCOM has been relying on an external authentication source. This has led to several problems in the past, including the inability to have different authentication providers simultaneously (for RSS clients, for example) and the integration problems of NemeinAuthentication and MidCOM Sessioning.

Therefore the implementation of a MidCOM authentication framework is probably the best way to counter this.

Authentication scenarios

  1. A user authenticates himself for work with the site. This could be done using any means, starting from Basic Auth and ending with a NemeinAuthentication like session-based system.
  2. Any program acting as a client of the site, must be allowed to use standardized authentication mechanisms (basically this breaks down to all forms of HTTP authentication). It is imperative, that this option stays available, as most RSS readers for example have serious trouble when authenticating against a Form/Cookie based system.
  3. Last, external programs acting as a client of the site might require the site to support special authentication procedures, like the MetaWebLog built in system.

This, in turn, leads to several scenarios where in MidCOM authentication can take place.

For a start, there is the classic form of authentication, which is used for all places where MidCOM can directly handle authentication. For that purpose, the site administrator sets a number of authorization back ends which are available for the site (see below for examples). This authentication procedure is the default for all cases on-site where components do not give additional information.

Each component may influence available authentication drivers during the handle phase if and only if the component would handle a request. In this circumstance, a component may return additional information at the end of the can_handle check. This can indicate one of two cases: For one hand, the component can activate additional authentication modules explicitly. MidCOM will then engage these modules before doing the authorization checks for a given topic. Alternatively, a component can indicate that it has just done a custom authentication for the given topic. MidCOM will then bypass any authentication procedure and go directly to the authorization checks.

Note, that authentication is checked after the can_handle phase for each topic. This means especially, that it is not inherited in any form whatsoever, as a component can only change authentication if it actually handles the request. For example, if a topic is hidden by ViewerGroups in the current authentication scope, no subtopic can enable a custom authentication whatsoever, as the parent viewer groups setting essentially hides the subtopic before it gets a chance to handle the request. This restriction is in place for both security (avoid executing code on a pure-luck basis) and simplicity (no need to take the authentication and authorization information across an entire subtopic). Of course, this might change in the future.

At each time during all of these scenarios, only a single authentication driver may be available at a given time. If a component, for example, activates HTTP Basic Auth, the previous driver gets disabled.

Authentication drivers

The midgard authentication driver mimics current behavior, by just taking authentication information from the Midgard Core on startup.

The http_basic driver uses HTTP Basic Auth when authentication a authentication is required. This is similar to the original Midgard behavior, with the difference that authentication is handles by MidCOM.

The http_cram driver uses HTTP CRAM-MD5 Authentication, but is otherwise identical to http_basic. Note, that Internet Explorer fails with CRAM-MD5 auth if and only if HTTP GET parameters are part of the request.

Finally, the session driver uses PHP sessioning in a way similar to NemeinAuth. When accessing a certain login URL, the system asks for a username and password and stores all required authentication values in a PHP session. This is the most transparent way to authenticate a user, but prevents automatic use of the site. The initial authentication is done using a HTTP Auth protected page. Depending on whether a GET parameter is in use Basic or CRAM-MD5 auth will be used at this point.

Require authentication mode

MidCOM will support a authentication requirement flag. This essentially prohibits anonymous access and enforces authentication using any authentication driver activated by the site admin. This mimics the original Midgard behavior when pages are set to Authenticate. This mode can be either by using a flag at a topic in the content tree or by calling an appropriate member function of the authentication service.

Implementation proposal

Encaspulation for Users, Groups and Virtual Groups

MidgardPersons and MidgardGroups are abstracted away for usage with the Authentication and Authorization API. They form their own hierarchy in the MidCOM core to allow for all additional features to be built in easily.

We will start from this class hierarchy:

class midcom_user; // Encaspulates MidgardPerson
class midcom_group; // Encaspulates MidgardGroup
class midcom_group_real extends midcom_group;
class midcom_group_virtual extends midcom_group;

Both base classes provide a set of interface methods to query all relevant information regarding the user tree.

In addition, The Virtual Group information needs a caching infrastructure, since especially things like the virtual group membership information is expensive to load. The same holds true for the effective permissions for a given user, which needs expensive queries to the user tree to sum up all permission information. Therefore the computed permissions for a given user and/or group should be cached as well.

The user and group base classes will have a number of static member functions which allow the user to query all these objects. The general pattern will follow the current MidgardAPI like midcom_group::list(...) as a replacement for the original mgd_list_groups. All member variables will be proxied into the classes so that the regular ->update semantics continue to work (this should be compatible with future MgdSchema driven releases).

Authentication/Authorization Service API draft

This is the actual service object, interacting with the components and the MidCOM core. It can be accessed as $midcom->auth.

class midcom_service_auth
{
// Update authentication information based on the current
// authentication driver.
void refresh();

// Checks whether a given user can read or write to a given
// object. At this time, read-checks are limited to topics,
// while write checks can be done with almost all
// MidgardObjects which support owner checks.
bool can_read(MidgardObject $object);
bool can_write(MidgardObject $object);

// Checks whether a user has sufficient permission for a given
// operation (see "operative permissions")
bool can_do(string $operative_permission_key, MidgardObject $object);

// Checks group membership of the current user, Parameter can
// be either an ID, GUID or MidgardGroup.
// This may also be one of the string based virtual group
// identifiers.
bool is_group_member(mixed $group);

// Add the same methods for other users then the current one
bool can_read(object $user, MidgardObject $object);
bool can_write(object $user, MidgardObject $object);
bool can_do(object $user, string $operative_permission_key, MidgardObject $object);
bool is_group_member(object $user, mixed $group);

// Checks if any valid user has authenticated himself
bool is_valid_user();

// The following functions are equivalent to their counterparts
// from above, with the difference that an access denied
// response is given to the browser using the current
// authentication driver, enforcing a re authentication controlled
// by that driver. So execution ends if a permission lacks.
//
// If you omit the message, some sensible access denied default
// is generated automatically.
void require_read(MidgardObject $object, string $message = "");
void require_write(MidgardObject $object, string $message = "");
void require_do(string $operative_permission_key, MidgardObject $object,
string $message = "");
void require_group_member(mixed MidgardGroup, string $message = "");
void require_valid_user(string $message="");

// Query or set the current authentication driver. A driver is
// identified by the last part of its class name
//
// When setting a new auth driver, it is automatically told to authenticate
// himself, which might exit under certain circumstances.
string get_auth_driver();
void set_auth_driver(string $key);

// Explicitly trigger an access denied error for the given reason. This
// usually triggers a re authentication.
// This call will exit.
//
// Note, that the exact behavior of this is dependent on the auth driver,
// and that with some authentication mechanisms the Reason cannot be
// displayed safely.
void access_denied(string $reason);

// Re authenticate with the credentials given
bool su (string $user, string $password);

// Revert to the initial authentication state as given by the current
// auth driver
//
// Note, that the exact behavior of this is dependent on the auth driver.
bool revert ();
}

All permissions for a given object can also be queried using the Metadata Interface, which holds the actual data. Inheritance is built by the central auth class though. Setting permissons is, too, done by using the Metadata Interface. Therefore the Metadata interface needs a few extensions:

class midcom_helper_metadtata
{
// ...

// This returns all defined privileges for the given object.
// It does not do any merging whatsoever. Returns Privilege records
Array get_privileges();

bool set_privilege (int $permission, object $assignee, int $value);
bool unset_privilee (int $permission, object $assignee);
}

A privilege record is defined as a simple accociative array:

Array $privilege
(
PRIVILEGE => string PRIVILEGE_NAME,
ASSIGNEE => mixed USER_OR_GROUP_IDENTIFIER,
VALUE => int PRIVILEGE_VALUE
);

Where the user or group identifier is some kind of guid/type mix to distinguish between persons, real and virtual groups and the privilege value is one of the following constants:

MIDCOM_PRIV_ALLOW = 1
MIDCOM_PRIV_DENY = 2
MIDCOM_PRIV_INHERIT = 3 /* internal flag */

Internally, the third state MIDCOM_PRIV_INHERIT is used indicating an unset privilege where applicable. Upon storage, INHERIT privileges are dropped from the corresponding object silently to minimize the number of parameters that need to be loaded.

The assigne will be constructed out of the type and the unique identifier of the assignee. For users and regular Midgard groups this will be the GUID, for virtual groups it will be the name of the virtual group that is used internally to identify it (which is largely equivalent to a GUID). Type and identifier are separated by a colon, so we get something in the line of user:$guid, group:$guid or vgroup:$vgroupid.

These arrays will be stored as parameters. The parameter name consists of the semicolon concatenated assignee/privilege string, while the value is one of the constants outlined above. Thus, a complete name could be user:$guid;net.nemein.downloads:set_release. Note, that INHERIT privileges are actually unset, as this is the default behavoir. They are stored in the domain midcom.services.auth.

For all content objects these privilege information is cached through the NAP cache implicitly.

Authentication driver API draft

All authentication drivers will inherit from a common base class (perhaps midcom_service_authdriver__base). The interface they present towards the auth service will look roughly like this:

class midcom_service_authdriver_xxx
{
// Process authentication credentials (or show some interface
// to the user asking him for credentials).
void authenticate();

// Displays an access denied error appropriate to the current
// authentication driver and automatically asks the user to
// enter a new set of credentials. Note, that this might prevent
// a message from being shown initially (like with basic auth,
// were the message is only shown if the cancel is hit).
void access_denied(string $reason);

// Revert the active Midgard Credentials to the state this
// authentication driver initially set them to.
void revert_credencials();
}

Sudo Service

The sudo service allows components to request admin privileges for operations requiring them (like creating persons or allow for anonymous posting). For security reasons, all components which are allowed to sudo have to be explicitly registered in the MidCOM configuration, which must also hold the credencials to use when sudoing. The requesting component is identified using the current component context, though this will require some thought with regard to pure code libraries.

Implementation Roadmap

If accepted, this service will be one of the Key features to be implemented in MidCOM 2.6 in Mid-Summer this year.

Other ideas

  • Enabling / Disabling of Accounts

Back

Designed by Nemein, hosted by Anykey