Extension:SimpleSecurity

From Organic Design wiki
Revision as of 02:57, 13 October 2007 by Nad (talk | contribs)

<?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.0, 2007-10-11');

  1. Global security settings

$wgSecurityMagic = "security"; # the parser-function name for security directives $wgSecurityMagicInherit = "!security"; # the name for security directives which apply to member articles $wgSecurityMagicIf = "ifallowed"; # the name for doing a permission-based conditional $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional $wgSecuritySysops = array('sysop'); # the list of groups whose members bypass all security (groups a all lowercase, user are ucfirst) $wgSecurityDenyTemplate = 'Template:Action not permitted'; $wgSecurityDenyReadTemplate = 'Template:Not readable'; $wgSecurityInfoTemplate = 'Template:Security info'; $wgSecurityRuleTemplate = ; # set to a template for the $wgSecurityDenyImage = dirname(__FILE__).'/deny.png'; # the image returned in place of requested image if read access denied $wgNamespacePermissions = array(); $wgSecurityLogActions = array('download'); # Actions that should be logged

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 {

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

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

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

$wgParser->setFunctionHook($wgSecurityMagic,array($this,'processDirective')); $wgParser->setFunctionHook($wgSecurityMagicInherit,array($this,'processInheritDirective',SFH_NO_HASH)); $wgParser->setFunctionHook($wgSecurityMagicIf,array($this,'ifAllowed')); $wgParser->setFunctionHook($wgSecurityMagicGroup,array($this,'ifGroup'));

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

# Process a normal security directive function processDirective(&$parser,$actions = ,$groups = ) { $parser->mOutput->mCacheTime = -1; if ($actions && $groups) $this->directives[] = array($actions,$groups); return ; }

# Process a non-inheriting security directive function processInheritDirective(&$parser,$actions = ,$groups = ) { $parser->mOutput->mCacheTime = -1; if ($actions && $groups) $this->directives[] = array($actions,$groups); return ; }

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

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

# ArticleAfterFetchContent hook: Asseses security after raw content fetched from database and clears if not readable # - also fills the global $securityCache cache with info to append to the rendered article function onArticleAfterFetchContent(&$article,&$text) { global $wgSecurityDenyTemplate; $title = $article->mTitle->getText(); if (!$this->activeHooks['ArticleAfterFetchContent']) return true; if (!$this->validateTitle('view',$article->mTitle)) $text = '{'.'{'."$wgSecurityDenyTemplate|fetch|$title}}"; return true; }

# Render any security info function onOutputPageBeforeHTML(&$out,&$text) { global $wgUser,$wgTitle,$wgSecurityInfoTemplate,$wgSecurityDenyTemplate, $wgSiteNotice,$wgSecurityParseInfo,$wgSecurityRuleTemplate,$wgSecurityLogActions; static $done = 0; if ($done++) return true; $psr = new Parser; $psropt = ParserOptions::newFromUser($wgUser);

# Add rules information for this article if any $rules = $this->rules[$this->title]; if (count($rules)) {

# Construct wikitext for info $info = '{'.'{'."$wgSecurityInfoTemplate|1=\n"; foreach ($rules as $rule) { $a = $rule[0] == '*' ? 'Every action' : ucfirst($rule[0]); $b = $rule[1] == '*' ? 'anybody' : ($rule[1] == 'user' ? 'logged in' : $rule[1]); $c = isset($rule[2]) ? $rule[2] : ; if ($wgSecurityRuleTemplate) $info .= '{'.'{'."$wgSecurityRuleTemplate|$a|$b|$c}}\n"; else $info .= "*$a requires the user to be $b $c\n"; } $info .= "\n}}";

                       # Parse the wikitext (depending on $wgSecurityParseInfo) and add to SiteNotice
                       if ($wgSecurityParseInfo) {
                               $psrout = $psr->parse($info,$wgTitle,$psropt,true,true);
                               $info = $psrout->getText();
                               }

$wgSiteNotice .= $info; }

# Replace main body with deny-message and append log if action allowed if (!$this->allowed) { $text = ; $psrout = $psr->parse('{'.'{'."$wgSecurityDenyTemplate|{$this->action}|{$this->title}}}",$wgTitle,$psropt,true,true); $out->mBodytext = $psrout->getText(); if (in_array($this->action,$wgSecurityLogActions)) { $msg = wfMsgForContent('securitylogdeny',$this->action,$this->title); $log = new LogPage('security',false); $log->addEntry('deny',$wgUser->getUserPage(),$msg); } }

return true; }

# Main validation code for the whole request is done on the first call to userCan function onuserCan(&$title, &$user, $ucaction, &$result) { global $wgTitle,$action,$wgRequest,$wgUser,$wgUploadDirectory, $wgSecurityEnableForImages,$wgSecurityDenyImage,$wgSecurityLogActions;

# Add IsAllowed hook to the $wgUser object static $done = 0; if ($done++ == 0) wfSimpleSecurityAddIsAllowedHook();

$this->title = $wgTitle->getPrefixedURL(); $this->action = $action == 'submit' ? 'edit' : strtolower($action);

# Moves need to be handled differently if ($this->title == 'Special:Movepage' && $this->action == 'submit') { $this->action = 'move'; $this->title = $wgRequest->getText('wpOldTitle',$wgRequest->getVal('target')); }

# Handle security on files (needs apache mod-rewrite) # - /wiki/images/... rewritten to article title Download/image-path... if (ereg('^Download/(.+)/([^/]+)$',$this->title,$match)) { $this->path = $match[1]; $this->file = $image = $match[2]; if (ereg('^thumb/.+/([^/]+)$',$this->path,$match)) $image = $match[1]; $wgTitle = Title::newFromText($this->title = "Image:$image"); $action = 'raw'; }

if ($this->initialised) { # we can call $this->validate in here so mediawiki can render links according to permissions } elseif (!empty($wgUser->mDataLoaded)) {

# Activate the hooks now that enough information is present to assess security $this->initialised = true; $this->activeHooks['ArticleAfterFetchContent'] = true;

# Validate current global action and set to view if not allowed if (!$this->allowed = $this->validateTitle($this->action,$wgTitle)) { $action = 'view'; global $wgEnableParserCache,$wgOut; $wgEnableParserCache = false; $wgOut->enableClientCache(false); }

# If the requested item is a file, return it to the client if security validates otherwise return the SecurityDenyImage if ($wgSecurityEnableForImages && $this->file) { if (in_array('download',$wgSecurityLogActions)) { $msg = wfMsgForContent('securitylogdeny','download',$this->title); $log = new LogPage('security',false); $log->addEntry('deny',$wgUser->getUserPage(),$msg); } $pathname = $this->allowed ? "$wgUploadDirectory/".$this->path.'/'.$this->file : $wgSecurityDenyImage; while (@ob_end_clean()); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.$this->file.'"'); $content = implode(,file($pathname)); if (in_array('Content-Encoding: gzip',headers_list())) $content = gzencode($content); echo($content); die; } } return true; }

# Return whether or not the passed action is permitted on the passed title # - uses $this->rules[title] as a cache function validateTitle($action,$title) { global $extramsg,$wgUser,$wgSecurityMagic,$wgSecurityMagicNoi,$wgSecurityEnableInheritance; $key = $title->getPrefixedURL(); if (isset($this->rules[$key])) $rules = $this->rules[$key]; else { # Disable the AfterDatabaseFetch hook to avoid an infinite loop and get the article text $this->activeHooks['ArticleAfterFetchContent'] = false; $this->activeHooks['ParserBeforeStrip'] = false; $article = new Article($title); $text = $article->getContent();

# Set up a new local parser object to process security directives independently of main rendering process $psr = new Parser; $psr->setFunctionHook($wgSecurityMagic,array($this,'processDirective')); if ($wgSecurityEnableInheritance) $psr->setFunctionHook($wgSecurityMagicNoi,array($this,'processDirectiveNoi')); $opt = ParserOptions::newFromUser($wgUser); $this->directives = array(); $out = $psr->parse($text,$title,$opt,false,true); $rules = $this->directives;

# Get the security items from the cats by running the parser over the content of each # - stop checking MagicNoi directives because they shouldn't inherit if ($wgSecurityEnableInheritance) { unset($psr->mFunctionHooks[$wgSecurityMagicNoi]); foreach ($out->getCategoryLinks() as $cat) { $ca = new Article($ct = Title::newFromText($cat = "Category:$cat")); $this->directives = array(); $psr->parse($ca->getContent(),$ct,$opt,false,true); foreach ($this->directives as $i) $rules[] = array($i[0],$i[1],"this rule is inherited from $cat"); } }

# Re-enable AfterFetch hook and cache the security info $this->activeHooks['ArticleAfterFetchContent'] = true; $this->activeHooks['ParserBeforeStrip'] = true; $this->rules[$key] = $rules; }

# Return the result of validating the extracted rules return $this->validateRules($action,$rules); }

# Return whether or not a user is allowed to perform an action according to an array of security items function validateRules($action,&$rules) { global $wgUser,$wgSecuritySysops; if (!is_array($rules)) return true;

# Resolve permission for this action from the extracted security links $security = ; foreach ($rules as $i) { #if ($i[1] == ) $i[1] = join(',',$wgSecuritySysops); $actions = preg_split("/\\s*,\\s*/",strtolower($i[0])); if (in_array($action,$actions) or (in_array('*',$actions) and $security == )) $security = $i[1]; }

# Get users group lists (add own username to groups) $groups = array_map('strtolower',$wgUser->getEffectiveGroups()); $groups[] = ucfirst($wgUser->mName); $security = $security ? preg_split("/\\s*,\\s*/",$security) : array();

# Calculate whether or not the action can be performed and return the result return ( count($security) == 0 or in_array('*',$security) or count(array_intersect($groups,$wgSecuritySysops)) > 0 or count(array_intersect($groups,$security)) > 0 ); }

# Validate the passed database row and replace any invalidate content function validateRules(&$row) { global $wgUser,$wgSecuritySysops,$wgSecurityDenyReadTemplate; }

}

  1. Add IsAllowed hook to the $wgUser object

function wfSimpleSecurityAddIsAllowedHook() { global $wgUser;

# Create a new User class ($User2) by extending the existing one with an overridden isAllowed method $User = get_class($wgUser); $User2 = $User.'2'; eval("class $User2 extends $User".' { function isAllowed($action = "") { $result = NULL; wfRunHooks("IsAllowed",array(&$this,$action,&$result)); return $result === NULL ? $result = parent::isAllowed($action) : $result; } }');

# Replace the $wgUser object with an identical $User2 instance $oldUser = $wgUser; $wgUser = new $User2(); foreach(array_keys(get_class_vars($User)) as $k) $wgUser->$k = $oldUser->$k; }

  1. Add DatabaseFetch and DatabaseQuery hooks

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

wfGetDB(); # This ensures that $wgLoadBalancer is not a stub object when we subclass it

# 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 $type = ucfirst($wgDBtype); eval("class Database{$type}2 extends Database{$type}".' {

public function query($sql,$fname = "",$tempIgnore = false) { $sql = preg_replace_callback("/(?<=SELECT ).+?(?= FROM)/","wfSimpleSecurityPatchSQL",$sql,1); return parent::query($sql,$fname,$tempIgnore); }

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

}');

# Create a replica of the LoadBalancer class which uses the new Database subclass for its connection objects $LoadBalancer = get_class($wgLoadBalancer); $LoadBalancer2 = $LoadBalancer."2"; eval("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;

wfSimpleSecurityAddDatabaseHooks();

$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.", 'securitylogdeny' => "Attempt to $1 $2 was denied.", 'securitylogentry' => "" )); }

}

  1. Needed in MediaWiki >1.8.0 for magic word hooks to work properly

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