Difference between revisions of "Extension:SimpleSecurity"

From Organic Design wiki
(mergeRights())
(method visibility keywords)
Line 37: Line 37:
  
 
# Constructor
 
# Constructor
function SimpleSecurity() {
+
function __construct() {
 
global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions,
 
global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions,
 
$wgSecurityMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup;
 
$wgSecurityMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup;
Line 60: Line 60:
 
# Enable/disable directive processing
 
# Enable/disable directive processing
 
# - $this->permissions is cleared when enabled
 
# - $this->permissions is cleared when enabled
function setDirectiveProcessing($enabled) {
+
private function setDirectiveProcessing($enabled) {
 
$this->processDirectives = $enabled;
 
$this->processDirectives = $enabled;
 
if ($enabled) $this->permissions = array();
 
if ($enabled) $this->permissions = array();
Line 66: Line 66:
  
 
# Process a security directive and save in $this->permissions (same format as $wgGroupPermissions)
 
# Process a security directive and save in $this->permissions (same format as $wgGroupPermissions)
function processDirective(&$parser,$actions = '',$groups = '',$allow = '') {
+
private function processDirective(&$parser,$actions = '',$groups = '',$allow = '') {
 
$parser->disableCache(); # Dont cache anything containing security directives
 
$parser->disableCache(); # Dont cache anything containing security directives
 
if ($actions == '' || $groups == '') return wfMsg('securitysyntaxerror');
 
if ($actions == '' || $groups == '') return wfMsg('securitysyntaxerror');
Line 79: Line 79:
  
 
# Process the ifUserCan conditional security directive
 
# Process the ifUserCan conditional security directive
function ifUserCan(&$parser,$action,$title,$then,$else = '') {
+
public function ifUserCan(&$parser,$action,$title,$then,$else = '') {
 
return $this->validateTitle($action,Title::newFromText($title)) ? $then : $else;
 
return $this->validateTitle($action,Title::newFromText($title)) ? $then : $else;
 
}
 
}
Line 85: Line 85:
 
# Process the ifGroup conditional security directive
 
# Process the ifGroup conditional security directive
 
# - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
 
# - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
function ifGroup(&$parser,$groups,$then,$else = '') {
+
public function ifGroup(&$parser,$groups,$then,$else = '') {
 
global $wgUser;
 
global $wgUser;
 
$intersection = array_intersect(array_map('strtolower',split(',',$groups)),$wgUser->getEffectiveGroups());
 
$intersection = array_intersect(array_map('strtolower',split(',',$groups)),$wgUser->getEffectiveGroups());
Line 92: Line 92:
  
 
# Ensure all groups are lowercase and add the username with first letter capitalised
 
# Ensure all groups are lowercase and add the username with first letter capitalised
function onUserEffectiveGroups(&$user,&$groups) {
+
public function onUserEffectiveGroups(&$user,&$groups) {
 
$groups = array_map('strtolower',$groups);
 
$groups = array_map('strtolower',$groups);
 
$groups[] = ucfirst($user->getName());
 
$groups[] = ucfirst($user->getName());
Line 102: Line 102:
 
# - the passed $rights array is a merge of $wgGroupPermissions and User::getEffectiveGroups()
 
# - the passed $rights array is a merge of $wgGroupPermissions and User::getEffectiveGroups()
 
# - $this->info is populated in this method
 
# - $this->info is populated in this method
function onUserGetRights(&$user,&$rights) {
+
public function onUserGetRights(&$user,&$rights) {
 
global $wgGroupPermissions,$wgTitle;
 
global $wgGroupPermissions,$wgTitle;
 
if (!is_object($wgTitle)) return true;
 
if (!is_object($wgTitle)) return true;
Line 160: Line 160:
  
 
# Update a rights-list based on passed user and permissions array
 
# Update a rights-list based on passed user and permissions array
function mergeRights(&$user,&$rights,&$permissions,$comment = false) {
+
static function mergeRights(&$user,&$rights,&$permissions,$comment = false) {
 
$groups = $this->getEffectiveGroups();
 
$groups = $this->getEffectiveGroups();
 
foreach ($groups as $g) if (isset($permissions[$g])) $rights = array_merge($rights,array_keys(array_filter($permissions[$g])));
 
foreach ($groups as $g) if (isset($permissions[$g])) $rights = array_merge($rights,array_keys(array_filter($permissions[$g])));
Line 167: Line 167:
  
 
# Render security info
 
# Render security info
function onOutputPageBeforeHTML(&$out,&$text) {
+
public function onOutputPageBeforeHTML(&$out,&$text) {
 
global $wgUser,$wgTitle,$wgVersion,$wgSiteNotice,$wgScriptPath,$wgSecurityDenyImage;
 
global $wgUser,$wgTitle,$wgVersion,$wgSiteNotice,$wgScriptPath,$wgSecurityDenyImage;
  
Line 200: Line 200:
 
}
 
}
  
# validate an action against a title within the context of the current user
+
# todo: validate an action against a title within the context of the current user
function validateTitle($action,$title) {
+
private function validateTitle($action,$title) {
 
if ($action == 'view') $action = 'read'; # for backward compatibility with old SimpleSecurity calls
 
if ($action == 'view') $action = 'read'; # for backward compatibility with old SimpleSecurity calls
 
return true;
 
return true;
Line 208: Line 208:
 
# Validate the passed database row and replace any invalid content
 
# Validate the passed database row and replace any invalid content
 
# - this does not get called if already processing directives
 
# - this does not get called if already processing directives
function validateDatabaseRow(&$row) {
+
private function validateDatabaseRow(&$row) {
 
global $wgUser,$wgSecuritySysops;
 
global $wgUser,$wgSecuritySysops;
  
Line 222: Line 222:
  
 
# Needed in some versions to prevent Special:Version from breaking
 
# Needed in some versions to prevent Special:Version from breaking
function __toString() { return 'SimpleSecurity'; }
+
public function __toString() { return __CLASS__; }
 
}
 
}
  

Revision as of 01:09, 30 December 2007

<?php

  1. Simple security extensionTemplate:Php
Info.svg These are the MediaWiki extensions we're using and/or developing. Please refer to the information on the mediawiki.org wiki for installation and usage details. Extensions here which have no corresponding mediawiki article are either not ready for use or have been superseded. You can also browse our extension code in our local Subversion repository or our GitHub mirror.
  1. - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
  2. - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
  3. - Needs apache's mod-rewrite for security on images, see code comments below
  4. - Version 4.0.0 started 2007-10-11

if (!defined('MEDIAWIKI')) die('Not an entry point.');

define('SIMPLESECURITY_VERSION','4.0.9, 2007-12-30');

  1. Global security settings

$wgSecurityMagic = "security"; # the parser-function name for security directives $wgSecurityMagicIf = "ifusercan"; # the name for doing a permission-based conditional $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional $wgSecurityDenyTemplate = 'Template:Action not permitted'; # Template used for displaying error when user violates permissions $wgSecurityInfoTemplate = 'Template:Security info'; # Template used to layout the security information $wgSecurityLogActions = array('download'); # Actions that should be logged $wgSecurityDenyImage = "/skins/monobook/lock_icon.gif"; # the image returned in place of requested image if read access denied $wgSecurityUseDBHook = true; # Add the DatabaseFetchHook to validate database access (experimental!)

array_unshift($wgExtensionFunctions,'wfSetupSimpleSecurity'); $wgHooks['LanguageGetMagic'][] = 'wfSimpleSecurityLanguageGetMagic'; $wgExtensionCredits['parserhook'][] = array( 'name' => "Simple Security", 'author' => 'User:Nad', 'description' => 'A simple to implement security extension', 'url' => 'http://www.mediawiki.org/wiki/Extension:Simple_Security', 'version' => SIMPLESECURITY_VERSION );

class SimpleSecurity {

var $processDirectives = false; # whether or not to process security directives when parsing wikitext var $permissions = array(); # permissions obtained from security directives (same format as $wgGroupPermissions) var $info = false; # accumulated security information accumulated from both directives and $wgGroupPermissions

# Constructor function __construct() { global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions, $wgSecurityMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup;

# Add our parser-hooks $wgParser->setFunctionHook($wgSecurityMagic,array($this,'processDirective')); $wgParser->setFunctionHook($wgSecurityMagicIf,array($this,'ifUserCan')); $wgParser->setFunctionHook($wgSecurityMagicGroup,array($this,'ifGroup'));

# Add all our required event hooks $wgHooks['UserGetRights'][] = $this; $wgHooks['UserEffectiveGroups'][] = $this; $wgHooks['OutputPageBeforeHTML'][] = $this;

# Add a new log type $wgLogTypes[] = 'security'; $wgLogNames ['security'] = 'securitylogpage'; $wgLogHeaders['security'] = 'securitylogpagetext'; $wgLogActions['security/deny'] = 'securitylogentry'; }

# Enable/disable directive processing # - $this->permissions is cleared when enabled private function setDirectiveProcessing($enabled) { $this->processDirectives = $enabled; if ($enabled) $this->permissions = array(); }

# Process a security directive and save in $this->permissions (same format as $wgGroupPermissions) private function processDirective(&$parser,$actions = ,$groups = ,$allow = ) { $parser->disableCache(); # Dont cache anything containing security directives if ($actions == || $groups == ) return wfMsg('securitysyntaxerror'); if ($this->processDirectives) { $allow = eregi('^(0|false)$',$allow) ? false : true; $actions = preg_split('/\\s*,\\s*/',$actions,-1,PREG_SPLIT_NO_EMPTY); $groups = preg_split('/\\s*,\\s*/',$groups,-1,PREG_SPLIT_NO_EMPTY); foreach ($groups as $g) foreach ($actions as $a) $this->permissions[$g][$a] = $allow; } return ; }

# Process the ifUserCan conditional security directive public function ifUserCan(&$parser,$action,$title,$then,$else = ) { return $this->validateTitle($action,Title::newFromText($title)) ? $then : $else; }

# Process the ifGroup conditional security directive # - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter public function ifGroup(&$parser,$groups,$then,$else = ) { global $wgUser; $intersection = array_intersect(array_map('strtolower',split(',',$groups)),$wgUser->getEffectiveGroups()); return count($intersection) > 0 ? $then : $else; }

# Ensure all groups are lowercase and add the username with first letter capitalised public function onUserEffectiveGroups(&$user,&$groups) { $groups = array_map('strtolower',$groups); $groups[] = ucfirst($user->getName()); return true; }

# Intercept the user rights and filter based on $wgGroupPermissions and $wgTitle # - this hook is called once the first time User::getRights is called # - the passed $rights array is a merge of $wgGroupPermissions and User::getEffectiveGroups() # - $this->info is populated in this method public function onUserGetRights(&$user,&$rights) { global $wgGroupPermissions,$wgTitle; if (!is_object($wgTitle)) return true; $this->info = ; $perm = array();

# Parse the article and add any directives to the list of perms to process ($perm) if ($wgTitle->exists()) { global $wgParser,$wgUser; if (is_object($wgParser)) { $psr = $wgParser; $opt = $wgParser->mOptions; } else { $psr = new Parser; $opt = NULL; } if (!is_object($opt)) $opt = ParserOptions::newFromUser($wgUser); $this->setDirectiveProcessing(true); $article = new Article($wgTitle); $psr->preprocess($article->getContent(),$wgTitle,$opt); $this->setDirectiveProcessing(false); self::mergeRights($user,$rights,$this->permissions,"parser function"); }

# Add x:x style permissions (Category, Namespace, Title, Match) to list of perms to process $cats = array(); foreach ($wgGroupPermissions as $k => $perms) if (preg_match('/^(.+?):(.*)$/',$k,$m)) { $type = strtolower($m[1]); $data = $m[2]; $merge = false; switch ($type) { case "category": if (count($cats) == 0) { # If processing first category rule, build a list of cats this article belongs to $dbr = &wfGetDB(DB_SLAVE); $cl = $dbr->tableName('categorylinks'); $id = $wgTitle->getArticleID(); $res = $dbr->select($cl,'cl_to',"cl_from = '$id'",__METHOD__,array('ORDER BY' => 'cl_sortkey')); while ($row = $dbr->fetchObject($res)) $cats[] == $row[0]; $dbr->freeResult($res); } $merge = in_array($cats,$data); break; case "namespace": $merge = $data == $wgTitle->getNamespace(); break; case "title": $merge = $data == $wgTitle->getText(); break; case "regex": $merge = preg_match($data,$wgTitle->getText()); break; default: wfRunHooks('SimpleSecurityPermissionFilter',array($type,$data,$merge)); }

# If current $wgGroupPermissions rule matches, merge it into the rights array if ($merge) self::mergeRights($user,$rights,$perms,"$type:$data"); }

return true; }

# Update a rights-list based on passed user and permissions array static function mergeRights(&$user,&$rights,&$permissions,$comment = false) { $groups = $this->getEffectiveGroups(); foreach ($groups as $g) if (isset($permissions[$g])) $rights = array_merge($rights,array_keys(array_filter($permissions[$g]))); #todo: add rule to $this->info and add ($comment) if $comment set }

# Render security info public function onOutputPageBeforeHTML(&$out,&$text) { global $wgUser,$wgTitle,$wgVersion,$wgSiteNotice,$wgScriptPath,$wgSecurityDenyImage;

# Add rules information for this article if any if ($this->info) {

# Parser the security info $info = $this->info; $this->info = false; $parser = new Parser; $info = $parser->parse($info,$wgTitle,ParserOptions::newFromUser($wgUser),true,true)->getText();

# Add some javascript to allow toggling the security-info $out->addScript("<script type='text/javascript'> function toggleSecurityInfo() { var info = document.getElementById('security-info'); info.style.display = info.style.display ?  : 'none'; } </script>");

# Add info-toggle before title and hidden info after title $pagetitle = $wgTitle->getText(); $alt = wfMsg('securityinfotoggle'); $toggle = "";

$info = "

";

$out->setPageTitle($toggle.$pagetitle.$info); $out->setHTMLTitle($pagetitle); #$out->mPageLinkTitle = $pagetitle; }

return true; }

# todo: validate an action against a title within the context of the current user private function validateTitle($action,$title) { if ($action == 'view') $action = 'read'; # for backward compatibility with old SimpleSecurity calls return true; }

# Validate the passed database row and replace any invalid content # - this does not get called if already processing directives private function validateDatabaseRow(&$row) { global $wgUser,$wgSecuritySysops;

# todo: find the current revision associated with the old_id

$this->setDirectiveProcessing(true); # todo: read and parse the revision text $this->setDirectiveProcessing(false);

# todo: validate the row and replace with wfMsg('securityreaddeny') if invalid

}

# Needed in some versions to prevent Special:Version from breaking public function __toString() { return __CLASS__; } }

  1. Adds UserGetRights and UserEffectiveGroups for MediaWiki versions prior to 1.11 which don't have them

function wfSimpleSecurityAddUserHooks() { global $wgVersion,$wgUser; if (version_compare($wgVersion,"1.11.0") <= 0) return;

# Create a new User class (User2) by extending the existing one with overridden methods class User2 extends User { function getRights() { parent::getRights(); wfRunHooks("UserGetRights",array($this,&$this->mRights)); } function getEffectiveGroups($recache = false) { $groups = parent::getEffectiveGroups($recache); wfRunHooks('UserEffectiveGroups',array(&$this,&$groups)); return $groups; } }

# If $wgUser is not of class User, it must be a StubUser which means we need to override _newObject to return a User2 instance $userclass = get_class($wgUser); if ($userclass != 'User') { class StubUser2 extends StubUser { function _newObject() { return wfCreateObject('User2',$this->mParams); } } }

# Replace $wgUser with a sub-classed replica $oldUser = $wgUser; $newclass = $userclass.'2'; $wgUser = new $newclass(); foreach (array_keys(get_class_vars($userclass)) as $k) $wgUser->$k = $oldUser->$k; }

  1. Hooks into Database::query and Database::fetchObject via the LoadBalancer class

function wfSimpleSecurityAddDatabaseHooks() { global $wgLoadBalancer,$wgDBtype;

# This ensures that $wgLoadBalancer is not a stub object when we subclass it # todo: this should be able to work in the case of it being a stub object wfGetDB();

# Create a replica of the Database class # - query method is overriden to ensure that old_id field is returned for all queries which read old_text field # - fetchObject method is overridden to validate row content based on old_id # - the changes to this class are only active for SELECT statements and while not processing security directives $type = ucfirst($wgDBtype); eval("class Database{$type}2 extends Database{$type}".' {

public function query($sql,$fname = "",$tempIgnore = false) { global $wgSimpleSecurity; $count = false; if (!$wgSimpleSecurity->processDirectives) $patched = preg_replace_callback("/(?<=SELECT ).+?(?= FROM)/","wfSimpleSecurityPatchSQL",$sql,1,$count); return parent::query($count ? $patched : $sql,$fname,$tempIgnore); }

function fetchObject(&$res) { global $wgSimpleSecurity; $row = parent::fetchObject($res); if (!$wgSimpleSecurity->processDirectives) $wgSimpleSecurity->validateDatabaseRow($row); return $row; }

}');

# Create a replica of the LoadBalancer class which uses the new Database subclass for its connection objects class LoadBalancer2 extends LoadBalancer { function reallyOpenConnection(&$server) { $server['type'] .= '2'; $db =& parent::reallyOpenConnection($server); return $db; } }

# Replace the $wgLoadBalancer object with an identical instance of the new LoadBalancer2 class $wgLoadBalancer->closeAll(); # Close any open connections as they will be of the original Database class $oldLoadBalancer = $wgLoadBalancer; $wgLoadBalancer = new LoadBalancer2($oldLoadBalancer->mServers); foreach (array_keys(get_class_vars('LoadBalancer')) as $k) $wgLoadBalancer->$k = $oldLoadBalancer->$k;

}

  1. Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field so permissions can be validated

function wfSimpleSecurityPatchSQL($match) { if (!preg_match("/old_text/",$match[0])) return $match[0]; $fields = str_replace(" ","",$match[0]); return ($fields == "*" || preg_match("/old_id/",$fields)) ? $fields : "$fields,old_id"; }

  1. Called from $wgExtensionFunctions array when initialising extensions

function wfSetupSimpleSecurity() { global $wgSimpleSecurity,$wgLanguageCode,$wgMessageCache,$wgSecurityMagic,$wgSecurityUseDBHook;

# Hooks into Database::query and Database::fetchObject via the LoadBalancer class if ($wgSecurityUseDBHook) wfSimpleSecurityAddDatabaseHooks();

# Adds UserGetRights and UserEffectiveGroups for MediaWiki versions prior to 1.11 which don't have them wfSimpleSecurityAddUserHooks();

# Instantiate the SimpleSecurity singleton now that the environment is prepared $wgSimpleSecurity = new SimpleSecurity();

# Add the messages used by the specialpage if ($wgLanguageCode == 'en') { $wgMessageCache->addMessages(array( 'security' => "Security log", 'securitylogpage' => "Security log", 'securitylogpagetext' => "This is a log of actions blocked by the SimpleSecurity extension.", 'securitylogentry' => "", 'securityinfotoggle' => "This article exhibits security restrictions. Click this icon for more detail.", 'securityreaddeny' => "!!Permission denied!!", 'securitysyntaxerror' => "{{#$wgSecurityMagic:Error: Both action and group must be specified!}}", 'securitylogdeny' => "Attempt to $1 $2 was denied." )); } }

  1. Register magic words

function wfSimpleSecurityLanguageGetMagic(&$magicWords,$langCode = 0) { global $wgSecurityMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup; $magicWords[$wgSecurityMagic] = array(0,$wgSecurityMagic); $magicWords[$wgSecurityMagicIf] = array(0,$wgSecurityMagicIf); $magicWords[$wgSecurityMagicGroup] = array(0,$wgSecurityMagicGroup); return true; }