Difference between revisions of "Extension:SimpleSecurity"

From Organic Design wiki
(4.0.2 - don't use wfRunHooks, just call the $wgSimpleSecurity methods directly)
(Remove the IsAllowed hook and use the UserGetRights way instead)
Line 12: Line 12:
 
# Global security settings
 
# Global security settings
 
$wgSecurityMagic            = "security";    # the parser-function name for security directives
 
$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
 
$wgSecurityMagicIf          = "ifallowed";    # the name for doing a permission-based conditional
 
$wgSecurityMagicGroup        = "ifgroup";      # the name for doing a group-based conditional
 
$wgSecurityMagicGroup        = "ifgroup";      # the name for doing a group-based conditional
Line 35: Line 34:
  
 
class SimpleSecurity {
 
class SimpleSecurity {
 
# Needed in some versions to prevent Special:Version from breaking
 
function __toString() { return 'SimpleSecurity'; }
 
  
 
# Constructor
 
# Constructor
 
function SimpleSecurity() {
 
function SimpleSecurity() {
 
global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions,
 
global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions,
$wgSecurityMagic,$wgSecurityMagicInherit,$wgSecurityMagicIf,$wgSecurityMagicGroup;
+
$wgSecurityMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup;
  
 
# Add all our required event hooks
 
# Add all our required event hooks
$wgHooks['userCan'][]                 = $this;
+
$wgHooks['UserGetRights'][] = $this;
$wgHooks['ArticleAfterFetchContent'][] = $this;
+
$wgHooks['UserEffectiveGroups'][] = $this;
$wgHooks['OutputPageBeforeHTML'][]    = $this;
 
  
 +
# Add our parser-hooks
 
$wgParser->setFunctionHook($wgSecurityMagic,array($this,'processDirective'));
 
$wgParser->setFunctionHook($wgSecurityMagic,array($this,'processDirective'));
$wgParser->setFunctionHook($wgSecurityMagicInherit,array($this,'processInheritDirective',SFH_NO_HASH));
 
 
$wgParser->setFunctionHook($wgSecurityMagicIf,array($this,'ifAllowed'));
 
$wgParser->setFunctionHook($wgSecurityMagicIf,array($this,'ifAllowed'));
 
$wgParser->setFunctionHook($wgSecurityMagicGroup,array($this,'ifGroup'));
 
$wgParser->setFunctionHook($wgSecurityMagicGroup,array($this,'ifGroup'));
Line 63: Line 58:
 
# Process a normal security directive
 
# Process a normal security directive
 
function processDirective(&$parser,$actions = '',$groups = '') {
 
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;
 
$parser->mOutput->mCacheTime = -1;
 
if ($actions && $groups) $this->directives[] = array($actions,$groups);
 
if ($actions && $groups) $this->directives[] = array($actions,$groups);
Line 76: Line 64:
  
 
# Process the ifAllowed conditional security directive
 
# Process the ifAllowed conditional security directive
function ifAllowed(&$parser,$action,$title,$if,$else = '') {
+
function ifAllowed(&$parser,$action,$title,$then,$else = '') {
return $this->validateTitle($action,Title::newFromText($title)) ? $if : $else;
+
return $this->validateTitle($action,Title::newFromText($title)) ? $then : $else;
 
}
 
}
  
 
# Process the ifGroup conditional security directive
 
# Process the ifGroup conditional security directive
# - evaluates to true if current uset belongs to any of the groups in $groups (csv)
+
# - 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,$if,$else = '') {
+
function ifGroup(&$parser,$groups,$then,$else = '') {
 
global $wgUser;
 
global $wgUser;
$intersection = array_intersect(
+
$intersection = array_intersect(array_map('strtolower',split(',',$groups)),$wgUser->getEffectiveGroups());
array_map('strtolower',split(',',$groups)),
+
return count($intersection) > 0 ? $then : $else;
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
+
# Ensure all groups are lowercase and add the username with first letter capitalised
# - also fills the global $securityCache cache with info to append to the rendered article
+
function onUserEffectiveGroups(&$user,&$groups) {
function onArticleAfterFetchContent(&$article,&$text) {
+
$groups = array_map('strtolower',$groups);
global $wgSecurityDenyTemplate;
+
$groups[] = ucfirst($user->getName());
$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;
 
return true;
 
}
 
}
  
# Main validation code for the whole request is done on the first call to userCan
+
# Intercept the user rights and filter based on $wgGroupPermissions and $wgTitle
function onuserCan(&$title, &$user, $ucaction, &$result) {
+
function onUserGetRights(&$user,&$rights) {
global $wgTitle,$action,$wgRequest,$wgUser,$wgUploadDirectory,
+
global $wgGroupPermissions,$wgTitle;
$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;
 
return true;
 
}
 
}
Line 197: Line 99:
 
}
 
}
  
}
+
# Needed in some versions to prevent Special:Version from breaking
 
+
function __toString() { return 'SimpleSecurity'; }
# 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;
 
 
}
 
}
  
Line 291: Line 174:
 
));
 
));
 
}
 
}
 
 
}
 
}
  
# Needed in MediaWiki >1.8.0 for magic word hooks to work properly
+
# Register magic words
 
function wfSimpleSecurityLanguageGetMagic(&$magicWords,$langCode = 0) {
 
function wfSimpleSecurityLanguageGetMagic(&$magicWords,$langCode = 0) {
global $wgSecurityMagic,$wgSecurityMagicInherit,$wgSecurityMagicIf,$wgSecurityMagicGroup;
+
global $wgSecurityMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup;
 
$magicWords[$wgSecurityMagic]        = array(0,$wgSecurityMagic);
 
$magicWords[$wgSecurityMagic]        = array(0,$wgSecurityMagic);
$magicWords[$wgSecurityMagicInherit] = array(0,$wgSecurityMagicInherit);
 
 
$magicWords[$wgSecurityMagicIf]      = array(0,$wgSecurityMagicIf);
 
$magicWords[$wgSecurityMagicIf]      = array(0,$wgSecurityMagicIf);
 
$magicWords[$wgSecurityMagicGroup]  = array(0,$wgSecurityMagicGroup);
 
$magicWords[$wgSecurityMagicGroup]  = array(0,$wgSecurityMagicGroup);

Revision as of 04:54, 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 $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 {

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

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

# Add our parser-hooks $wgParser->setFunctionHook($wgSecurityMagic,array($this,'processDirective')); $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 the ifAllowed conditional security directive function ifAllowed(&$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 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 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 function onUserGetRights(&$user,&$rights) { global $wgGroupPermissions,$wgTitle; 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; }

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

  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. 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; } ?>