Difference between revisions of "Extension:SimpleSecurity"

From Organic Design wiki
m
(4.0.2 - don't use wfRunHooks, just call the $wgSimpleSecurity methods directly)
Line 8: Line 8:
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
 
   
 
   
define('SIMPLESECURITY_VERSION','4.0.0, 2007-10-11');
+
define('SIMPLESECURITY_VERSION','4.0.2, 2007-10-13');
  
 
# Global security settings
 
# Global security settings
Line 156: Line 156:
 
static $done = 0;   
 
static $done = 0;   
 
if ($done++ == 0) wfSimpleSecurityAddIsAllowedHook();
 
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)
 
# Handle security on files (needs apache mod-rewrite)
Line 176: Line 167:
 
}
 
}
  
if ($this->initialised) {
+
# If the requested item is a file, return it to the client if security validates otherwise return the SecurityDenyImage
# we can call $this->validate in here so mediawiki can render links according to permissions
+
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;
 
}
 
}
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 true;
 
}
 
}
  
# Return whether or not the passed action is permitted on the passed title
+
# Validate the passed database row and replace any invalidate content
# - uses $this->rules[title] as a cache
+
function validateDatabaseRow(&$row) {
function validateTitle($action,$title) {
+
global $wgUser,$wgSecuritySysops,$wgSecurityDenyReadTemplate;
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
+
# Validate the passed action against the current user and title
function validateRules(&$row) {
+
function validateAction(&$user,$action) {
global $wgUser,$wgSecuritySysops,$wgSecurityDenyReadTemplate;
+
global $wgSecuritySysops;
 
}
 
}
  
Line 300: Line 208:
 
eval("class $User2 extends $User".' {
 
eval("class $User2 extends $User".' {
 
function isAllowed($action = "") {
 
function isAllowed($action = "") {
$result = NULL;
+
global $wgSimpleSecurity;
wfRunHooks("IsAllowed",array(&$this,$action,&$result));
+
$result = $wgSimpleSecurity->validateAction($this,$action);
 
return $result === NULL ? $result = parent::isAllowed($action) : $result;
 
return $result === NULL ? $result = parent::isAllowed($action) : $result;
 
}
 
}
Line 309: Line 217:
 
$oldUser = $wgUser;
 
$oldUser = $wgUser;
 
$wgUser  = new $User2();
 
$wgUser  = new $User2();
foreach(array_keys(get_class_vars($User)) as $k) $wgUser->$k = $oldUser->$k;
+
foreach (array_keys(get_class_vars($User)) as $k) $wgUser->$k = $oldUser->$k;
 
}
 
}
  
Line 354: Line 262:
 
$oldLoadBalancer = $wgLoadBalancer;
 
$oldLoadBalancer = $wgLoadBalancer;
 
$wgLoadBalancer  = new $LoadBalancer2($oldLoadBalancer->mServers);
 
$wgLoadBalancer  = new $LoadBalancer2($oldLoadBalancer->mServers);
foreach(array_keys(get_class_vars($LoadBalancer)) as $k) $wgLoadBalancer->$k = $oldLoadBalancer->$k;
+
foreach (array_keys(get_class_vars($LoadBalancer)) as $k) $wgLoadBalancer->$k = $oldLoadBalancer->$k;
  
 
}
 
}

Revision as of 03:26, 13 October 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.2, 2007-10-13');

  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();

# 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 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; }

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

# Validate the passed action against the current user and title function validateAction(&$user,$action) { global $wgSecuritySysops; }

}

  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 = "") { global $wgSimpleSecurity; $result = $wgSimpleSecurity->validateAction($this,$action); 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; } ?>