This is the implementation reference for the Cookbook.UserAuth2 recipe. It is valid for the 2.0 release version.

Permission table master index

 #...,            (comment; note that the comma in the end is necessary)
 @<group>         (indicates the membership in and interprets the entailing rights
                   of <group> as acquiring all of them; honoured at the place of occurrence)
 *                (allow everything)
 rd_...           (allowed to read
 ed_...                       edit
 up_...                       upload
 hi_...                       view history (page changes))
 xx_...           (joker level matching on any page related level)
 pr               (allowed to change profiles)
 pw               (allowed to change its own password)
 ps               (allowed to set passwords of users below)
 ad               (allowed access to admin tool, for changing user rights etc)
 cu               (allowed to create new/ delete user/group)
 eu               (allowed to edit user/group permissions)
 ip               (allowed to change ip related login restrictions)    (rd is placeholder for any page related level here)
 rd_*.*a???b*     (some complex page name pattern with usual wild cards ? and *)
 -rd_...          (negation of the specified right)
 ...{$AuthId}...  (in LoggendInUsers record, this is replaced by upper cased version of user name)

 -@...            not allowed
 -*               not allowed

Permission evaluation process

When calling the UserAuth() function, it is decided based on authprompt whether some page should be printed out (then go to TryAccessingPage()) or whether just a permission query takes place (then HasCurrentUserPerm()). In the latter case the following stages are encountered (in this order) to obtain the final permission result:

  1. HasCurrentUserPerm: Asking query cache for result if already existing
  2. HasCurrentUserPermUncached: Silent granting right by ip address
  3. CheckUserPerms: Identity based hardwired permission checking
  4. CheckUserPermsWithRecord: Perm checking based on user-specific record, examining perm tables

The following table summarizes the spec for evaluating the permission result (P_c(p,l)). The pair (p,l) refers to the queried page and level, and c to the connecting client. The t(g,u) is the permission table granted by granter g to user u.

Client permission
P_c(p,l) = Or_{ip ranges i} (matches(i, address(c)) and R_i(p,l)) 
           (authenticated(c) and (R_{LoggedInUsers}(p,l) or R_{username(c)}(p,l))
User permissionR_u(p,l) = Or_{granter g, with g is patron of u} (R_g(p,l) and S_{t(g,u)}(p,l))
Recursion seedR_{admin}(p,l) = true, if (p,l) is valid
Permission by Table
S_t(p,l) = { S_{t'}(p,l), if the last entry of t is not applicable  
             true, if last entry is applicable and affirmative
             false, if last entry is applicable and denying
             S_{t'}(p,l) or R_m(p,l), if entry grants membership in group m
(here t' is the table t with the last entry removed, and S_{emptytable}(p,l)=false)

Note that when $UA2AllowMultipleGranters is false (default), you will always have only one granter (the parent), and thus the multiple Or in R_u(p,l) reduces to its argument. Note also that the granter must be a patron of the user concerned - arbitrary "granting around" is not allowed.

Public functions provided

 function UserAuth($pagename, $level, $authprompt=true) // for interfacing/compatibility
 function UserAuth2($pagename, $level, $authprompt=true)
 function TryAccessingPage($pagename, $level)
 function HasCurrentUserPerm($page, $level)
 function HasCurrentUserPermForAction($page, $action)
 // wrapper which employs $HandleAuth to resolve action to level
 function CheckUserPerms($user, $page, $level, $groupaction = false)
 function CheckUserPermsForAction($user, $page, $action, $groupaction = false)
 // wrapper which employs $HandleAuth to resolve action to level
 function mayCurrAdminActOnPermHolder($action, $admin_action, $tool_username, $groupaction = false)

Directory structure


Data structures

Permission record

Array with keys:

 description =>
 parent =>
 loginFromIpsOnly =>            (array of ip ranges; if undefined, then check disabled)
 perms => [granter1 => ...,
           granter2 => ...,

User profile

Array with keys:

 password =>                    (in crypt()ed form)
 cookiekey =>                   (stores the random key of the authentication cookie)
 cookiekeycreatetime =>         (... and that's expiration)
 email =>                       (not yet implemented)
 notification =>                (not yet implemented)

Session data

Array with keys:

 firststarttime                             administrative keys for security and expiration
 POST_data => (key1 => val1, ...)           carried-over _POST data upon session expiration
 cachestarttime                             when the perm record and query cache was last purged
 username                                   empty string when not logged in
 auth_message                               what appears on page when login fails
 editlocks => (filename1 => locktime, ...)  what permission records the client currently edits
 profile                                    currently not used
 iprangerecords => (iprange1 => record, ...)
 grouppermrecords => (group1 => record, ...)
 userpermrecords => (user1 => record, ...)
 permqueries => ("level page" => auth_result)  page/level pairs with their respective permission
                                               result for the current user (with the current 
                                               authentication status)
 target_url                                 where to redirect in the follow-up of a forced login
 prev_contentpage                           previous content page browsed (= where to redirect
                                            after a self-inititaed login)

Markup generated

 (:loginform:)                  (as before)
 (:if loggedin:)                (as before)
 (:if memberOf <group>:)        (untested)
 (:if member @<group>:)         (for backward compatibility, untested)
 (:if ipMatches <range>:)

Functions/Variables of interest

Pmwiki variables manipulated

 $AuthFunction             w    (set to "UserAuth")
 $AuthId                   w
 $HandleAuth               a
 $HandleActions            w

Pmwiki actions (re)defined

SDVA($HandleAuth, array(
 // action => level
  'admin'            => 'admin',
  'pwchange'         => 'pwchange',
  'pwset'            => 'pwset',
  'profile'          => 'profile',    // not used at the moment
  'diff'             => 'history',   
  'edituserperms'    => 'edituserperms',   // just relevant UA2-internally
  'createuserperms'  => 'createuserperms', //
  'setipperms'       => 'setipperms'       //

Global variables generated

 $IsUserLoggedIn           read-only by other modules
 $OnCreateUserFunc         writable

Some configuration variables

(see code for complete list and detailed description)

SDV($UA2EnforceFixedClientIp,    true);     
SDV($UA2SiteIdentifier,          __FILE__);       
SDV($UA2SessionMaxInactivityTime, 2*60*60); // In seconds, default 2 hours.
SDV($UA2SessionMaxLifeTime,      24*60*60); // In seconds, default 1 day.
SDV($UA2AllowPostCompletion,     true);     

SDV($UA2EnablePermCaching,       true); // might be useful to set to false during debugging
SDV($UA2MaxPermRecordCacheSize,  20);
SDV($UA2MaxPermQueryCacheSize,   100);

SDV($LoginPage,                  "Site.Login");
SDV($HomePage,                   "Main.HomePage");
SDV($UA2AfterSILoginRedirectTo,  ''); // default '' = previous content page
SDV($UA2AfterLogoutRedirectTo,   $LoginPage);
SDV($UA2AdminLoginFromIpsOnly,   ''); // default '' = no restriction

SDV($UA2AllowCookieLogin,        false);
SDV($UA2CookiePrefix,            '');
SDV($UA2CookieExpireTime,        60*60*24*30); // cookie default expiration in seconds
SDV($UA2LoginLockMsgFmt,         ''); 

SDV($UA2AllowMultipleGranters,   false); // this option is honoured only on the UI level
SDV($UA2PermEditLockTimeout,     60*60*12); 

PHP directives manipulated

ini_set('', 'PHPSESSID' . strtoupper(md5($UA2SiteIdentifier)));
ini_set('session.save_path', $UA2SessionSavePathDir);
ini_set('session.cache_expire', max($UA2SessionMaxLifeTime/60, ini_get('session.cache_expire')));
ini_set('session.gc_maxlifetime', max($UA2SessionMaxLifeTime, ini_get('session.gc_maxlifetime')));

Valid strings

Everything case sensitive:

  • User names: letters, digits, underscore; must begin with letter
  • User group names: letters, digits, underscore; must begin with letter
  • Page names: letters, digits


perm semantics:

I. Hierarchy
- every user and every group belongs to a direct (single) parent
- a parent is always a proper user (not a group)
- a granter must be always a proper user (not a group)
- admin is root parent
- as chaining is possible, a user can effectively have many (indirect) parents (called patrons)
- the user may be granted rights from each of its patrons (independently);
  thus the users rights consist of the or-conjunction of all these rights
- a patron can grant only rights it possess itself; this rule is enforced
- a group membership is only honoured if the granter is patron of the group (#201)

- a patron can view and alter the perms of all of its children (if "ad" right
  is granted), including the ones that are granted by intermediate patrons

II. Special users
There are special users, "admin", "GuestUser" (for unauthenticated users), 
"LoggedInUser" (for any user that is authenticated). For these

- all users are granted at least the GuestUser rights
- all logged in users are granted at least LoggedInUser rights
  (these two points are only valid for proper users, i.e. not for groups;
   since we ultimately will check permissions always for proper users,
   the guest user or logged in user privileges will come into play anyway
   at the very beginning of traversing the perm hierachy; see #004)
- "admin" can do everything

- apart from 'admin' also the special users have parents and all the regular
  perm records, so can be assigned to be administered by non-'admin' users
  as well.
- but: the guest and logged in user group can be created or deleted only by 'admin'

III. Interpretation of perm table
- default: deny all
- interpretation starts from the top of the perm table, leaving all decls
  in order
- if location specifier matches, then adjusts the current perm result
- granted group memberships are honoured in their place of occurrence, but only
  in affirmative direction (as of 2.0-beta5)
- note that the final result will always be masked by the permission
  of the direct parent

IV. Hardwired rights:
- the login page is readable by everyone
[- the home page is readable by everyone (used for redirection)] (removed as of 2.0-beta4, #010)

V. Policies on the alteration of permissions
- the current admin can see only his children
- the current admin can (see? and) change permissions only of his children
- the current admin can see permissions granted by anyone to his children
  (this can entail only the other patrons of these children, since anyone else has
  no right (and no power) to grant permissions to the children)
- the current admin can set the parent of the user only to himself or users 
  which are his children (since a user has always less rights than his parent,
  this avoids extending permissions by switching to a higher parent)
  [parent switching could also be completely forbidden]

VI. Specialities introduced by groups:
- A granter may grant membership only to groups he is a patron of. (#201)
- When interpreting the GuestUsers table for non-logged-in users, all admin tool related
actions should be denied, since these would be bound to introduce inconsistencies in the 
perm system since a defined parent is usually needed for admin actions. To have this,
the 'admin' action (only, for speed) is denied in CheckUserPermsWorker() (targetting UI
issued queries), and the actual enforcement of ALL admin related actions is done in 
mayCurrentAdminActOnPermHolder() (we rely on that only this is used everywhere in the 
admin tool for permission checks, which is true). (#301)

Valid characters etc.

- user and group names are case sensitive
- ... may contain only letters and digits (no underscores), must start with letter
- reserved names are case insensitive versions of 'admin' and the guest user group
  and logged in user group
- user and group names are implementationally separated (can be distinguished), 
  but still on creation are forbidden to conflict

- you cant login as group or ip range
- groups (and ip ranges) have no profile
- groups (and ip ranges) have no loginFromIpsOnly field set

- on every page request, it is checked once (at the beginning) whether the permissions
  have been updated
- auth cookie is deleted when user logs out or - not reachable - the "persistent" checkbox is disabled.
- an admin edit session is started whenever the client enters the edit form of some user and finished
  when logging out. (or after saving or when pressing cancel in the form, still to implement)
- a new empty profile is created (together with an empty perm record) whenever a new user is created;
  the inital password is the empty password

- the variable $origuser is used to "feed through" the user name for which the outer-most 
(root) permission query was started, to the end of having it when replacing the {$AuthId} 
occurences in the LoggedInUsers perm record, #011
- make sure on creation of a user/group that it does not yet exist, both with upper- and
lower cased first character; this assures that we can identify a valid page name with each user
(which has to be ucfirst) without possibility of conflict; (needed for implementing {$AuthId}
in perm tables); #012

Remarks on certain constructions

Permission cache

Permission evaluation results (perm queries) and loaded permission records are cached to speed up the evaluation process ("L1" and "L2" cache). Set $UA2EnablePermCaching to false to disable it. $UA2MaxPermQueryCacheSize and $UA2MaxPermRecordCacheSize control the respective sizes of the caches (20 and 100 by default). Using a time stamp, the perm cache gets invalidated every time someone changes something in the permission setup.

Edit locks

The edit locks prevent admins from editing one single permission record concurrently. Edit locks are released whenever (a) the admin logs out, (b) presses 'cancel' in the edit form, or (c) the edit lock just expires. By default, the edit locks expires after 12 hours; use $UA2PermEditLockTimeout to control this.


It can happen the when specifying group memberships, a circular dependence of perm records is created. To avoid infinite recursion, a calling stack is used which is filled up with users/groups examined so far. Upon occurence of a circle, the permission evaluation is bound and a negative result is locally returned.


The redirection is accomplished using two variables, target_url and prev_contentpage, used for redirection after triggered or self-initiated login, respectively. In the first case, the client accessed a resource and was found to be not privileged and not authenticated. After login he is then directed to that resource, if privileged. In the second case, the client just happened to want to login himself, and is by default redirected to the last content page (browse level) after successful authentication, unless otherwise stated via $UA2AfterSILoginRedirectTo option.


In the code there are many functions taking a "user" parameter as argument and have a parameter "groupaction" defaulting to false. The meaning is that if groupaction is true, the user argument is actually to be interpreted as group (a user group).