Difference between revisions of "Extension:SimpleSecurity"
m (cat) |
(start $wgGroupPermissions switch) |
||
| Line 1: | Line 1: | ||
<?php | <?php | ||
| − | # Simple security extension{{php}}{{Category:Extensions | + | # Simple security extension{{php}}{{Category:Extensions|SimpleSecurity4}} |
# - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details | # - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details | ||
# - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html) | # - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html) | ||
# - Needs apache's mod-rewrite for security on images, see code comments below | # - Needs apache's mod-rewrite for security on images, see code comments below | ||
| − | # - Version 4.0.0 started 2007-10-11 | + | # - Version 4.0.0 started 2007-10-11 |
if (!defined('MEDIAWIKI')) die('Not an entry point.'); | if (!defined('MEDIAWIKI')) die('Not an entry point.'); | ||
| − | define('SIMPLESECURITY_VERSION','4.0. | + | define('SIMPLESECURITY_VERSION','4.0.8, 2007-12-23'); |
# Global security settings | # Global security settings | ||
| − | $wgSecurityMagic | + | $wgSecurityMagic = "security"; # the parser-function name for security directives |
| − | $wgSecurityMagicIf | + | $wgSecurityMagicIf = "ifusercan"; # the name for doing a permission-based conditional |
| − | $wgSecurityMagicGroup | + | $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional |
| − | $wgSecurityDenyTemplate | + | $wgSecurityDenyTemplate = 'Template:Action not permitted'; # Template used for displaying error when user violates permissions |
| − | $wgSecurityInfoTemplate | + | $wgSecurityInfoTemplate = 'Template:Security info'; # Template used to layout the security information |
| − | + | $wgSecurityLogActions = array('download'); # Actions that should be logged | |
| − | $wgSecurityLogActions | + | $wgSecurityDenyImage = "/skins/monobook/lock_icon.gif"; # the image returned in place of requested image if read access denied |
| − | $wgSecurityDenyImage | + | $wgSecurityUseDBHook = true; # Add the DatabaseFetchHook to validate database access (experimental!) |
array_unshift($wgExtensionFunctions,'wfSetupSimpleSecurity'); | array_unshift($wgExtensionFunctions,'wfSetupSimpleSecurity'); | ||
| Line 47: | Line 47: | ||
# Add all our required event hooks | # Add all our required event hooks | ||
| − | $wgHooks['UserGetRights'][] = $this; | + | $wgHooks['UserGetRights'][] = $this; |
| − | $wgHooks['UserEffectiveGroups'][] = $this; | + | $wgHooks['UserEffectiveGroups'][] = $this; |
$wgHooks['OutputPageBeforeHTML'][] = $this; | $wgHooks['OutputPageBeforeHTML'][] = $this; | ||
| Line 66: | Line 66: | ||
# Process and store a security directive | # Process and store a security directive | ||
function processDirective(&$parser,$actions = '',$groups = '',$allow = '') { | function processDirective(&$parser,$actions = '',$groups = '',$allow = '') { | ||
| − | $parser->mOutput->mCacheTime = -1; | + | $parser->mOutput->mCacheTime = -1; # Dont cache anything containing security directives |
if ($actions == '' || $groups == '') return wfMsg('securitysyntaxerror'); | if ($actions == '' || $groups == '') return wfMsg('securitysyntaxerror'); | ||
if ($this->processDirectives) { | if ($this->processDirectives) { | ||
| Line 73: | Line 73: | ||
$this->info .= "*$actions($groups)\n"; | $this->info .= "*$actions($groups)\n"; | ||
} | } | ||
| − | $foo = $this->processDirectives ? 'on' : 'off'; | + | $foo = $this->processDirectives ? 'on' : 'off'; # todo: rv |
return "<br>done($foo)<br>"; | return "<br>done($foo)<br>"; | ||
} | } | ||
| Line 111: | Line 111: | ||
$this->setDirectiveProcessing(false); | $this->setDirectiveProcessing(false); | ||
| − | # todo: validate $wgTitle from directive state and $wgGroupPermissions | + | # todo: validate $wgTitle from directive state and |
| + | |||
| + | # Filter rights based on $wgGroupPermissions (Category, Namespace, Title, Match) | ||
| + | foreach (array_keys($wgGroupPermissions) as $perm) | ||
| + | if (preg_match('/^(.+?):(.*)$/',$perm,$m)) { | ||
| + | $type = strtolower($m[1]); | ||
| + | $data = $m[2]; | ||
| + | switch ($type) { | ||
| + | case "category": | ||
| + | $dbr = &wfGetDB(DB_SLAVE); | ||
| + | $cl = $dbr->tableName('categorylinks'); | ||
| + | $id = Title:newFromText($data)->getArticleID(); | ||
| + | $res = $dbr->select($cl,'cl_to',"cl_from = '$id'",__METHOD__,array('ORDER BY' => 'cl_sortkey')); | ||
| + | while ($row = $dbr->fetchObject($res)) $titleText = Title::newFromID($row[0])->getText(); | ||
| + | $dbr->freeResult($res); | ||
| + | break; | ||
| + | case "namespace": | ||
| + | $wgTitle->getNamespace() | ||
| + | break; | ||
| + | case "title": | ||
| + | # title | ||
| + | break; | ||
| + | case "regex": | ||
| + | #regexp | ||
| + | break; | ||
| + | default: wfRunHooks('SimpleSecurityPermissionFilter',array($type,$data)); | ||
| + | } | ||
| + | } | ||
# todo: store the validation results to use for future calls | # todo: store the validation results to use for future calls | ||
| Line 121: | Line 148: | ||
} | } | ||
| − | # Render | + | # Render security info |
function onOutputPageBeforeHTML(&$out,&$text) { | function onOutputPageBeforeHTML(&$out,&$text) { | ||
global $wgUser,$wgTitle,$wgVersion,$wgSiteNotice,$wgScriptPath,$wgSecurityDenyImage; | global $wgUser,$wgTitle,$wgVersion,$wgSiteNotice,$wgScriptPath,$wgSecurityDenyImage; | ||
| Line 193: | Line 220: | ||
function getEffectiveGroups($recache = false) { | function getEffectiveGroups($recache = false) { | ||
$groups = parent::getEffectiveGroups($recache); | $groups = parent::getEffectiveGroups($recache); | ||
| − | wfRunHooks('UserEffectiveGroups',array(&$this,&$groups)); | + | wfRunHooks('UserEffectiveGroups',array(&$this,&$groups)); |
return $groups; | return $groups; | ||
} | } | ||
| Line 273: | Line 300: | ||
# Called from $wgExtensionFunctions array when initialising extensions | # Called from $wgExtensionFunctions array when initialising extensions | ||
function wfSetupSimpleSecurity() { | function wfSetupSimpleSecurity() { | ||
| − | global $wgSimpleSecurity,$wgLanguageCode,$wgMessageCache,$wgSecurityMagic; | + | global $wgSimpleSecurity,$wgLanguageCode,$wgMessageCache,$wgSecurityMagic,$wgSecurityUseDBHook; |
# Hooks into Database::query and Database::fetchObject via the LoadBalancer class | # Hooks into Database::query and Database::fetchObject via the LoadBalancer class | ||
| − | wfSimpleSecurityAddDatabaseHooks(); | + | if ($wgSecurityUseDBHook) wfSimpleSecurityAddDatabaseHooks(); |
# Adds UserGetRights and UserEffectiveGroups for MediaWiki versions prior to 1.11 which don't have them | # Adds UserGetRights and UserEffectiveGroups for MediaWiki versions prior to 1.11 which don't have them | ||
| Line 291: | Line 318: | ||
'securitylogpagetext' => "This is a log of actions blocked by the [[MW:Extension:SimpleSecurity|SimpleSecurity extension]].", | 'securitylogpagetext' => "This is a log of actions blocked by the [[MW:Extension:SimpleSecurity|SimpleSecurity extension]].", | ||
'securitylogentry' => "", | 'securitylogentry' => "", | ||
| − | 'securityinfotoggle' | + | 'securityinfotoggle' => "This article exhibits security restrictions. Click this icon for more detail.", |
'securityreaddeny' => "!!Permission denied!!", | 'securityreaddeny' => "!!Permission denied!!", | ||
'securitysyntaxerror' => "<nowiki>{{#$wgSecurityMagic</nowiki>:'''Error:''' Both ''action'' and ''group'' must be specified!}}", | 'securitysyntaxerror' => "<nowiki>{{#$wgSecurityMagic</nowiki>:'''Error:''' Both ''action'' and ''group'' must be specified!}}", | ||
| Line 307: | Line 334: | ||
return true; | return true; | ||
} | } | ||
| − | |||
Revision as of 05:55, 23 December 2007
<?php
- Simple security extensionTemplate:Php
- - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
- - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
- - Needs apache's mod-rewrite for security on images, see code comments below
- - Version 4.0.0 started 2007-10-11
if (!defined('MEDIAWIKI')) die('Not an entry point.');
define('SIMPLESECURITY_VERSION','4.0.8, 2007-12-23');
- 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 $info = false; var $processDirectives = false; var $directives = array();
# Constructor function SimpleSecurity() { 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 function setDirectiveProcessing($enabled) { $this->processDirectives = $enabled; if ($enabled) $this->directives = array(); }
# Process and store a security directive
function processDirective(&$parser,$actions = ,$groups = ,$allow = ) {
$parser->mOutput->mCacheTime = -1; # Dont cache anything containing security directives
if ($actions == || $groups == ) return wfMsg('securitysyntaxerror');
if ($this->processDirectives) {
$allow = eregi('^(0|false)$',$allow) ? false : true;
$this->directives[] = array($actions,$groups,$allow);
$this->info .= "*$actions($groups)\n";
}
$foo = $this->processDirectives ? 'on' : 'off'; # todo: rv
return "
done($foo)
";
}
# Process the ifUserCan conditional security directive 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 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,$wgUser,$wgParser;
# The first time this is called we need to validate $wgTitle static $processed = 0; if ($processed++ == 0) {
# Process the directives in $wgTitle's content by expanding its templates $this->setDirectiveProcessing(true); $article = new Article($wgTitle); $wgParser->preprocess($article->getContent(),$wgTitle,new ParserOptions); $this->setDirectiveProcessing(false);
# todo: validate $wgTitle from directive state and
# Filter rights based on $wgGroupPermissions (Category, Namespace, Title, Match) foreach (array_keys($wgGroupPermissions) as $perm) if (preg_match('/^(.+?):(.*)$/',$perm,$m)) { $type = strtolower($m[1]); $data = $m[2]; switch ($type) { case "category": $dbr = &wfGetDB(DB_SLAVE); $cl = $dbr->tableName('categorylinks'); $id = Title:newFromText($data)->getArticleID(); $res = $dbr->select($cl,'cl_to',"cl_from = '$id'",__METHOD__,array('ORDER BY' => 'cl_sortkey')); while ($row = $dbr->fetchObject($res)) $titleText = Title::newFromID($row[0])->getText(); $dbr->freeResult($res); break; case "namespace": $wgTitle->getNamespace() break; case "title": # title break; case "regex": #regexp break; default: wfRunHooks('SimpleSecurityPermissionFilter',array($type,$data)); } }
# todo: store the validation results to use for future calls }
# todo: filter $rights based on stored validation results
return true; }
# Render security info 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; }
# validate an action against a title within the context of the current user 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 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 function __toString() { return 'SimpleSecurity'; } }
- 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; }
- 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;
}
- 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"; }
- 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." )); } }
- 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; }



