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:
- Midgard Documentation, Concepts and Features, Managing Users (this text does also deal with access control)
- Midgard Documentation, API Reference, Groups
- Midgard Documentation, API Reference, Persons
- Midgard Documentation, API Reference, Members
- OpenPSA 2.0 Core Specification Draft
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
- 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.
- 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.
- 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
