SecLayer

Summary: Provide a ready-made security layer (while developing recipes) for controlling page access
Version: 2015-06-06
Prerequisites: 2.2.x beta, PHP5
Status: Beta
Maintainer: Peter Bowers
Users: +1 (View / Edit)

Recipes using this Tool: WikiSh, WikiBox

Questions answered by this recipe

This section is optional; use it to indicate the types of questions (if any) this recipe is intended to answer.

  • How can my fancy new recipe get an added layer of security regarding page access while still keeping administration as simple as possible for the administrator?

Description

A tool for recipe developers to aid in adding a security layer.

  • Often developers want to allow administrators to have a greater degree of control in setting up security for recipes. This recipe provides that functionality.
  • Note that no author/administrator functionality is provided in this recipe. This recipe provides a tool for other recipe developers to facilitate development of the functionality contained in their recipes.

Installation

Installation will be dependent on the needs of the recipe using SecLayer. However, do note that SecLayer.php includes stdconfig.php and thus should occur in config.php (or farmconfig.php) after any configuration lines that are related to stdconfig.php -- $EnableXyz and etc.

Notes

Security is determined by a combination of 4 elements: (1) page pattern, (2) authorization level, (3) (optional) priority, and (4) (optional) user ID. To facilitate administration users can be combined into @groups and you can set up aliases for one or more authorization levels. Note that SecLayer does not require a certain set of authorization levels - the recipe using SecLayer defines which authorization levels are valid.

As a reference, here is a definition of what is contained in each of these 4 elements:

  • page pattern(s)
    • either a page specification or a wildcard specification
    • an optional ! or - prefix can be placed before a page or wildcard specification to indicate exclusion/negation
    • multiple instances of this can be contained within a single field separated by commas
    • EXAMPLE:
      • GroupA.*,GroupB.Page,-GroupC.ExcludedPage
  • authorization level(s)
    • any authorization token the recipe author chooses to use (no whitespace allowed)
    • edit, read, and attr are familiar to most PmWiki administrators, but a recipe author can define any set of authorizations he/she wishes to use
    • authorization tokens can be optionally preceded by a ! or - to indicate exclusion/negation of this particular authorization
    • multiple authorization tokens can be contained within a single field separated by commas
    • EXAMPLE:
      • read,modify,-create,-delete
  • priority
    • normally a single-digit number, optional
    • if not specified it defaults to 5
    • lower numbers are "higher" priority
  • user ID(s)
    • if a site is using authuser.php then user IDs can be specified so a given authorization can be given/withheld from a certain user(s).
    • a - prefix can be used, but simply to exclude a user from a set of users (usually when a @group is used and one or more users should NOT be part of that group for this particular authorization record).
    • note that specifying a - before a user ID does NOT negate the authorization -- it simply removes that user from consideration on the current record
    • EXAMPLE:
      • sam,joe,sally
      • @admins,-sally

These elements can all be set up on an auth-config page as colon-delineated fields. This auth-config page is then parsed via a call to slParsePage() (usually in config.php) prior to any calls to slAuthorized(). If an administrator prefers to set things up in config.php rather than on an auth-config page then he/she may do so via calls to slAddAuth().

A typical config.php making use of SecLayer might look like this:

include_once("cookbook/RecipeX.php");
include_once("cookbook/SecLayer.php");
slParsePage($pagename, "SiteAdmin.RecipeXAuth", $AuthRecipeX);
slAddAuth($AuthRecipeX, "SiteAdmin.SecretPage", "edit", 1, "joe,sally");

(The specifics of a particular recipe's use of SecLayer will have to be defined by that recipe -- that example config.php is purely hypothetical.)

Here is what a simple auth-config page might look like (accepting the defaults for user IDs and priority):

Test.*:read,edit
Group.*:read,edit
Group.VitalPage:-edit
(:if ! auth admin:)
Group.Secret:-read,-edit
(:endif:)

What this mean is that any file in the Test group can be read or edited by the calling recipe. And any page in the Group group can be read and edited EXCEPT that Group.VitalPage does NOT allow editing and the page Group.Secret allows neither reading nor editing unless you ar.

The entire page (or section) will be processed using the "if", "comment", and "include" rules before slParsePage() starts to see what authorizations are actually contained there. This allows you to do conditionals based on (:if auth admin:) to give certain authorizations only to the administrator or (:if name Foo.Bar:) if you wanted certain authorizations to be present only when a certain page was being loaded, etc.

If an administrator prefers to use a syntax which approaches page-specification in (:pagelist...:) you can also include/exclude pages like this to provide a functionally identical auth-config page:

Test.*:read,edit
Group.*:read,edit
-Group.VitalPage:edit
(:if ! auth admin:)
-Group.Secret:read,edit
(:endif:)

As you can see the negation (exclusion) can be applied either on the page patterns or on the authorization levels. One says "for this page (pattern) you are not allowed to do this action" while the other says "when authorizing this action exclude this page pattern". (It is possible to negate both the page pattern and the authorization level resulting in a POSITIVE INCLUSION (2 negatives equals a positive), but that's just plain confusing and not recommended unless it's really neeeded.)

Aliases and groups can be defined by use of the "=" operator. What this means is that any given token will be expanded to another set of tokens. So this definition:

@admins = jack,sally,sam

means that every time the token @admins occurs (for instance, in a user list) it will be replaced by those users. You are not limited to user groups -- an alias can also be defined for a set of authorization privileges or a set of page patterns:

all = read,edit,attr
none = -read,-edit,-attr
adminfiles = SiteAdmin.*,Site.Myprivatepage

Aliases and groups are synonyms. By convention the "@" character is used as a prefix for groups of users. Be aware that this is up to the administrator - it is not something enforced by SecLayer. (In other words, you are free to put the @ before groups or not and you can also put the @ in front of other aliases - you'll just confuse anybody else that tries to administer your site...)

Aliases and groups can be defined recursively, with one alias containing another alias. They will all be expanded as expected. If a given definition would become an infinite loop then the situation will be prevented, but the name of the alias will end up as a token. For instance, in these definitions:

most = read,edit
all = most,attr
@groupA = sam, @groupB
@groupB = jack, @groupA

The token "all" will now become "read, edit, attr" after "more" is expanded.

The token "@groupA" will contain "sam, jack, @groupA" and the token "@groupB" will contain "jack, sam, @groupB". Leaving the group is necessary in a case like this to prevent an infinite loop.

Let's define another auth-config page, this time including a @group and a couple aliases as well as introducing the user field:

@admins = jack,sally,sam
all = read,edit,attr
none = -read,-edit,-attr

Test.*:all::@admins
Group.*:read,edit::@admins,-sam
Group.VitalPage:-edit
Group.Secret:none

As you can see, the fact that we specify users on one line doesn't mean we need to do so on all lines. The rule is simple -- whenever the user field is not included it will default to a user '*' which means all users.

Note that negation is handled differently between users and other fields. A negated user removes that user entirely from that authorization - not that he is negatively authorized (proscribed) but that that user is simply removed from that context of authorization. Negation in a page or authorization context, on the other hand, actively negates the authorization being provided. This is an important consideration when the priority field is in use or when all of a given group except one or two will be given a certain authorization.

In this example:

@powerusers = sam, jack, sally
SiteAdmin.PageX:edit::@powerusers, -jack
GroupA.*:-edit::jack
-GroupB.*:edit::jack

Jack has specifically been denied authorization from any pages in GroupA or GroupB. (In one the authorization was negated and in the other the page was negated.) However, for SiteAdmin.PageX his authorization was simply not defined. This means that a lower priority authorization could still authorize him. (If no other line authorizes him he will still not be authorized because there has been no explicit authorization for SiteAdmin.PageX.)

In general, SecLayer operates on this rule:

If I find a matching exclusion or I don't find an explicit matching inclusion then there's no authorization. If I find an explicit matching inclusion and no matching exclusion then that action will be authorized.

This means that an auth-config page like this will not allow writing to any page in the SiteAdmin group, even to the MyRecipe page:

SiteAdmin.*:-read,-edit,-attr
SiteAdmin.MyRecipe:read,edit

This is because the presence of the matching exclusion always over-rides any possible inclusion. Because of this problem there SecLayer also has a concept of priorities:

SiteAdmin.*:-read,-edit,-attr:2
SiteAdmin.MyRecipe:read,edit:1

This means that the inclusive pattern for MyRecipe is at a higher priority than the exclusive pattern for SiteAdmin.* and so we will not get to the lower priority pattern at all. The generalized rule above becomes a bit more complicated when priorities are introduced.

Process through priority levels in ascending order (0-9). If I find a matching exclusion on a given priority level then authorization fails. If I find an explicit matching inclusion and no matching exclusion on that given priority level then that action will be authorized - no further priority levels will be considered. If I find neither inclusion nor exclusion on a given priority level then we advance to the next priority level and apply the same rule again. If there are no further priority levels and I still haven't found an explicit matching inclusion then there's no authorization.

slParsePage() takes 3 arguments:

  1. a reference page
  2. the name of the auth-config page
  3. the array in which the authorization table will be stored (existing values are preserved)

slAddAuth() takes 5 arguments:

  1. the array in which the authorization table will be stored
  2. page pattern(s), as specified in the first field of the auth-config page
  3. authorization level(s), as specified in the second field of the auth-config page
  4. priority, as specified in the 3rd field of the auth-config page
  5. user(s), as specified in the 4th field of the auth-config page

Once the array containing the authorization table has been initialized using either/both of slParsePage() and slAddAuth() then the recipe can make use of slAuthorized() function prior to reads/writes of pages to make sure that the security layer is enforced.

slAuthorized() takes 3 arguments:

  1. the name of the page you wish to access (should already be normalized via MakePageName, FmtPageName, etc.)
  2. the array in which the authorization table was stored
  3. the authorization level you need to carry out the given action

3 other functions are provided as wrapper functions for the basic PmWiki functions UpdatePage() and RetrieveAuthPage(). These wrappers will enforce SecLayer security prior to calling the basic functions. These functions follow:

slUpdatePage()

  1. pagename (as usual)
  2. oldpage (as usual)
  3. newpage (as usual)
  4. the array in which the authorization table was stored
  5. SecLayer authorization being requested (defaults to 'edit')

slUpdateAuthPage()

  1. pagename (as usual)
  2. oldpage (as usual)
  3. newpage (as usual)
  4. the array in which the authorization table was stored
  5. SecLayer authorization being requested (defaults to 'edit')
  6. PmWiki authorization which will be confirmed via CondAuth() prior to calling UpdatePage()

slRetrieveAuthPage()

  1. pagename (as usual)
  2. authorization level (as usual)
  3. prompt for password (as usual)
  4. since (as usual)
  5. the array in which the authorization table was stored
  6. SecLayer authorization being requested (defaults to 'edit')

Note that for some of these wrapper functions you will need to specify arguments which were previously default (in the base function) in order to specify arguments which follow. There could still be some movement in the order of these arguments to minimize this type of difficulty.

Troubleshooting

Since SecLayer is a powerful tool you can sometimes get yourself into complicated situations where it is difficult to confirm exactly which user does or does not have a given authorization for a given page. There is a Markup() available to help in situations like these. Put this code in your config.php:

Markup('xyzauthpage', '<{$var}',
   '/\(:xyzAuthPage:\)/ie', 
   'slDisplayAuth($pagename, $GLOBALS["xyzAuthPage"])');

Replace "xyz" with the appropriate prefix to make sense within your context. For instance, if I were debugging WikiSh then it would be $GLOBALS['wshAuthPage'] and (optionally, but it makes sense) you would use wshauthpage for name of the markup rule and '/\(:wshAuthPage:\)/ie' for the markup pattern. But the important thing is to change the name within $GLOBALS[...] to match the name of the variable which holds the array containing the authorization table for your recipe.

Release Notes

If the recipe has multiple releases, then release notes can be placed here. Note that it's often easier for people to work with "release dates" instead of "version numbers".

  • 2015-06-06: Implemented Markup_e() for PHP 5.4 compatibility (actually markup is commented out, but for completeness)
  • 2009-11-14: Fixed split() issue with PHP 5.3. Also fixed a problem where $FarmD was not used in accessing a script.
  • 2009-04-19: Fixed a problem with infinite recursion, fixed a problem with spaces preceding authorization. Minor bug fixes.
  • 2008-08-21: Added markup rule processing (defaults to if, include, and comment) to allow in-page processing during slParsePage.
  • 2008-07-31: Added MakePageName() to fix case problems, allowed capability to read a section of a page in slParsePage(), duplicates are now prevented in the underlying data structure, an slDisplayAuth() with markup [@(:xyzAuthPage:) was added to help understand what's going on in the underlying data structure, bug fixed with positive inclusion after negative exclusion, further work on slUpdateAuthPage().
  • 2008-06-12: Cleaned up some unnecessary debugging info
  • 2008-06-10: Added capability of recursive alias/group definitions, made aliases and user group synonymous. Defined special handling of negation of users.
  • 2008-06-05: Initial release, still under testing and development. NOT ready for integration with any recipes yet unless you want to participate in testing.

See Also

Contributors

The basic approach to the auth-config page is borrowed from fox.

Comments

User notes +1: If you use, used or reviewed this recipe, you can add your name. These statistics appear in the Cookbook listings and will help newcomers browsing through the wiki.