Extension:PermissionACL

From MediaWiki.org
Jump to navigation Jump to search
MediaWiki extensions manual
OOjs UI icon advanced.svg
PermissionACL
Release status: beta
Implementation User rights
Description Implements per namespace, page, category, user or user group access permissions
Author(s) Jan Vavříček (Vavricektalk)
Database changes No
License GPL
Download See the Code section
  • $wgPermissionACL
  • $wgPermissionACL_Superuser
Translate the PermissionACL extension if it is available at translatewiki.net
Check usage and version matrix.

The PermissionACL extension implements a way to restrict access to specific {namespaces, pages, categories} based on user group or user name. This provides a more fine grained security model than the one provided by the default $wgGroupPermissions.

PermissionACL extension configuration is based on ACL (Access Control List) - list of rules which are processing from first to last. First applicable rule is used! On the end of list is implicit rule DENY TO ALL!

How secure is this extension?[edit]

Security issues with authorization extensions
Inclusion/transclusion safe - If Extension:RemoveProtectedContent is used as well
Preloading ?
XML export (Special:Export) safe
Atom/RSS feeds ?
Listings & search safe - If Extension:RemoveProtectedContent is used as well
Diff & revision links ?
API ?
Action links safe
Related rights ?
Author backdoor safe
Caching ?
Files & Images ?
Redirects ?
Edit Section safe
Watching Pages unsafe
Other extensions ?
Raw/render ?
Recentchanges ?
Known bugs Configuration is case-sensitive (e.g. Special:Userlogin is different to Special:UserLogin)

Usage[edit]

If $wgPermissionACL is set, then ACL model is used - if not, the extension will do nothing.

Rules are array elements and their order in array is used by ACL mechanism.

Syntax of rules (every rule has 4 parts):

  1. which page : select of pages (namespaces, categories)
  2. which user : select of users (groups)
  3. which action : select of actions ( userCan actions - read, edit, create, move)
  4. operation : permit or deny access

First, second and third rule part can be:

  • one value
  • array of values
  • ALL (represented by asterisk)

Summary of syntax[edit]

The following may be repeated multiple times to add rules to the ACL:

$wgPermissionACL[] = array(
                           {'group' | 'user'}, => {<username> | <groupname> | '*'} [, {<username> | <groupname> | '*'}...] ,
                           {'namespace' | 'page' | 'category'} =>  <namespace> [, <namespace>...] ,
                           'action'    => {'read', 'edit', 'create', 'move', '*'} [, {'read', 'edit', 'create', 'move', '*'}...] ,
                           'operation' => {'permit', 'deny'}
                          );

Example[edit]

This example configures for the scenario:

  • Namespaces: Private, Ccna, Ccnp, Ns, Fwl
  • User groups: private, ccna, ccnp, ns, fwl;
  • Group ccna has RW access only to namespace Ccna, group fwl to Fwl, ...
  • Group private has RW access to every namespace
  • Unlogged users can only read NS_MAIN namespace
  • Administrators (users "wikisysop" and "vav166") can do everything
require_once("$IP/extensions/PermissionACL/PermissionACL.php");
$wgExtraNamespaces = array( 100 => "Private",
                            101 => "Private_Talk",
                            102 => "Ccna",
                            103 => "Ccna_Talk",
                            104 => "Ccnp",
                            105 => "Ccnp_Talk",
                            106 => "Ns",
                            107 => "Ns_Talk",
                            108 => "Fwl",
                            109 => "Fwl_Talk" );

$wgGroupPermissions['ccna']['read'] = true;
$wgGroupPermissions['ccnp']['read'] = true;
$wgGroupPermissions['ns']['read']   = true;
$wgGroupPermissions['fwl']['read']  = true;
$wgGroupPermissions['private']['read'] = true;

//whitelist is used, but same thing can be done by ACL:
/*
$wgPermissionACL[] = array('group'     => '*',
                           'page'      =>  array('Special:UserLogin', 'Special:UserLogout', 'Special:Resetpass', 'Special:Confirmemail'),
                           'action'    => 'read',
                           'operation' => 'permit');
*/
$wgWhitelistRead = array('Special:Userlogin', 'Special:Userlogout', 'Special:Resetpass', 'Special:Confirmemail');

//Superuser is only simplification - same result will have ACL:
/*
$wgPermissionACL[] = array('user'      =>  array('wikisysop', 'vav166'),
                           'page'      => '*',
                           'action'    => '*',
                           'operation' => 'permit');
*/
$wgPermissionACL_Superuser = array('wikisysop', 'vav166');

$wgPermissionACL[] = array('group'     => '*',
                           'namespace' =>  NS_MAIN,
                           'action'    => 'read',
                           'operation' => 'permit');

$wgPermissionACL[] = array('group'     => 'user',
                           'namespace' =>  array(NS_MAIN, NS_SPECIAL, NS_USER, NS_CATEGORY),
                           'action'    => 'read',
                           'operation' => 'permit');

$wgPermissionACL[] = array('group'     => 'private',
                           'namespace' =>  array(100, 101, 102, 103, 104, 105, 106, 107, 108, 109),
                           'action'    => '*',
                           'operation' => 'permit');

$wgPermissionACL[] = array('group'     => 'ccna',
                           'namespace' =>  array(102, 103),
                           'action'    => '*',
                           'operation' => 'permit');

$wgPermissionACL[] = array('group'     => 'ccnp',
                           'namespace' =>  array(104, 105),
                           'action'    => '*',
                           'operation' => 'permit');

$wgPermissionACL[] = array('group'     => 'ns',
                           'namespace' => array(106, 107),
                           'action'    => '*',
                           'operation' => 'permit');

$wgPermissionACL[] = array('group'     => 'fwl',
                           'namespace' => array(108, 109),
                           'action'    => '*',
                           'operation' => 'permit');

Download instructions[edit]

Please cut and paste the code found below and place it in $IP/extensions/PermissionACL/PermissionACL.php.

Note: $IP stands for the root directory of your MediaWiki installation, the same directory that holds LocalSettings.php.

Installation[edit]

To install this extension, add the following to LocalSettings.php:

require_once("$IP/extensions/PermissionACL/PermissionACL.php");

#add configuration parameters here
#setup user rights here

Code[edit]

<?php

if( !defined( 'MEDIAWIKI' ) ) {
    echo("This file is an extension to the MediaWiki software and cannot be used standalone.\n");
    die(1);
}

$wgExtensionCredits['other'][] = array(
        'name' => 'PermissionACL',
        'author' => 'Jan Vavříček',
        'url' => 'https://mediawiki.org/wiki/Extension:PermissionACL',
        'description' => 'Provides a permissions access control list',
);

$wgHooks['userCan'][] = 'PermissionACL_userCan';

function PermissionACL_userCan($title, $user, $action, &$result) {
    global $wgPermissionACL, $wgPermissionACL_Superuser, $wgWhitelistRead;

    $result = NULL;

    if(!isset($wgPermissionACL)) return TRUE; //if not set - grant access

    if($title->isCssJsSubpage()) return TRUE;

    if($action == 'read' && is_array($wgWhitelistRead)) { //don't continue if - read action on whitelisted pages
        if(in_array($title->getPrefixedText(), @$wgWhitelistRead)) {
            $result = TRUE;
            return TRUE;
        }//if-in_array
    }//if-whitelist

    if(isset($wgPermissionACL_Superuser)) { //Superuser can do everything - no need to read ACLs
        if(is_array($wgPermissionACL_Superuser)) {
            if(in_array(strtolower($user->getName()), ArrayToLower($wgPermissionACL_Superuser))) return TRUE;
        }//if-superusers array
        else if(strtolower($user->getName()) == strtolower($wgPermissionACL_Superuser)) return TRUE;
    }//if-superuser

    foreach($wgPermissionACL as $rule) { //process ACLs
        if(!PermissionACL_isRuleValid($rule)) continue; //syntax checking

        if(PermissionACL_isRuleApplicable($rule, $title, $user, $action)) {
            if(PermissionACL_Permit($rule)) {
                $result = TRUE;
                return TRUE;
            }
            else {
                $result = FALSE;
                return FALSE;
            }
        }//if-applicable
    }//foreach

    // If the user's trying to "read"/"edit" a page which doesn't exist; 
    // check for "create" and "createpage" actions
    if (
        (!($title->isKnown())) &&
        (
         ($action == 'read') ||
         ($action == 'edit')
        )
       ) {
      if (
          PermissionACL_userCan($title, $user, 'create', $result) ||
          PermissionACL_userCan($title, $user, 'createpage', $result)
         ) {
        return $result;
      }
    }

    //implicit end rule - DENY ALL
    $result = FALSE;
    return FALSE;
}//PermissionACL_userCan

function PermissionACL_isRuleValid($rule) {
    /* rule parts:
        'group' || 'user'
        'namespace' || 'page' || 'category'
        'action' = (read, edit, create, move, *)
        'operation' = (permit, deny)
    */
    $tmp_actions    = array('read', 'edit', 'create', 'move', '*');
    $tmp_operations = array('permit', 'deny');

    if (isset($rule['group']) ^ isset($rule['user'])) {
        if (isset($rule['namespace']) ^ isset($rule['page']) ^ isset($rule['category'])) {
            if (isset($rule['action'])  && ((is_string($rule['action']) && in_array($rule['action'], $tmp_actions)) || is_array($rule['action']) )) {
                if (isset($rule['operation']) && in_array($rule['operation'], $tmp_operations)) {
                    return TRUE;
                }//if-operation test
            }//if-action test
        }//if-namespace, page, category test
    }//if-user, group test
    
    return FALSE;
}//function PermissionACL_isRuleValid

function PermissionACL_isRuleApplicable($rule, $title, $user, $action) {
    //group|user rule
    if(isset($rule['group'])) { //group rule
        if(is_array($rule['group']))
            $tmp = ArrayToLower($rule['group']);
        else
            $tmp = strtolower($rule['group']);

        $groups = ArrayToLower($user->getEffectiveGroups());
        if(!( (is_string($tmp) && in_array($tmp, $groups)) ||
              (is_array($tmp) && count(array_intersect($tmp, $groups))>0)
            )) return FALSE;
    }
    else { // user rule
        if(is_array($rule['user']))
            $tmp = ArrayToLower($rule['user']);
        else
            $tmp = strtolower($rule['user']);
        $tmp2 = strtolower($user->getName());

        if(!( (is_string($tmp) && $tmp=='*') ||
              (is_string($tmp) && $tmp==$tmp2) ||
              (is_array($tmp) && in_array($tmp2, $tmp))
            )) return FALSE;
    }

    //namespace|page|category rule
    if(isset($rule['namespace'])) { //namespace rule
        $tmp = $rule['namespace'];
        $tmp2 = $title->getNamespace();

        if(!( (is_int($tmp) &&  $tmp==$tmp2) ||
              (is_string($tmp) && $tmp=='*') ||
              (is_array($tmp) && in_array($tmp2, $tmp))
            )) return FALSE;
    }
    else if(isset($rule['page'])){ //page rule
        $tmp = $rule['page'];
        $tmp2 = $title->getPrefixedText();

        if(!( (is_string($tmp) && $tmp==$tmp2) ||
              (is_string($tmp) && $tmp=='*') ||
              (is_array($tmp) && in_array($tmp2, $tmp))
            )) return FALSE;
    }
    else { //category rule
        $tmp = $rule['category'];
        $tmp2 = $title->getParentCategories();
        $categs = array();

        if(is_array($tmp2)) {
            global $wgContLang;
            $tmp_pos = strrpos($wgContLang->getNSText(NS_CATEGORY), ':');

            foreach($tmp2 as $cat => $page) {
                if($tmp_pos === FALSE) {
                    $categs[] = substr($cat, strpos($cat, ':')+1);
                }
                else {
                    $tmp_categ = substr($cat, $tmp_pos+1);
                    $categs[] = substr($tmp_categ, strpos($tmp_categ, ':')+1);
                }
            }
        }

        if(!( (is_string($tmp) && is_array($tmp2) && in_array($tmp, $categs)) ||
              (is_string($tmp) && $tmp=='*') ||
              (is_array($tmp)  && is_array($tmp2) && count(array_intersect($tmp, $categs))>0)
            )) return FALSE;
    }

    //action rule
    if(is_array($rule['action']))
        $tmp = ArrayToLower($rule['action']);
    else
        $tmp = strtolower($rule['action']);

    return ($tmp == $action) ||
          (is_string($tmp) && $tmp=='*') ||
          (is_array($tmp) && in_array($action, $tmp));
}//function PermissionACL_isRuleApplicable

function PermissionACL_Permit($rule) {
    return $rule['operation'] == 'permit';
}//function PermissionACL_Permit

function ArrayToLower($ar) {
    $tmp = array();
    foreach($ar as $index => $value)
        $tmp[$index] = strtolower($value);
    return $tmp;
}//function ArrayToLower