r92364 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r92363‎ | r92364 | r92365 >
Date:16:09, 16 July 2011
Author:btongminh
Status:reverted (Comments)
Tags:
Comment:
First steps for bug 14801: add backend support for per-namespace permissions to core. This extends $wgGroupPermissions syntax from $wgGroupPermissions[$group][$right] = bool to $wgGroupPermissions[$group][$right] = array( NS_X => bool ). This is safely backwards compatible; the booleans are still fully supported, and any unset namespace will default to false.

* User::getRights(), User::isAllowed() and User::getGroupPermissions now optionally accept a namespace parameter. If not set, it will check whether the user has the right for all namespaces.
* Anything that uses Title::getUserPermissionsErrorsInternal() automatically supports per-namespace permissions. This includes Title::getUserPermissionsErrors and Title::(quick)UserCan.
* Fix tests that set User::mRights

The next step would be to change all User::isAllowed() to Title::quickUserCan or pass the namespace to User::isAllowed().
Modified paths:
  • /trunk/phase3/RELEASE-NOTES-1.19 (modified) (history)
  • /trunk/phase3/includes/DefaultSettings.php (modified) (history)
  • /trunk/phase3/includes/Title.php (modified) (history)
  • /trunk/phase3/includes/User.php (modified) (history)
  • /trunk/phase3/tests/phpunit/includes/ArticleTablesTest.php (modified) (history)
  • /trunk/phase3/tests/phpunit/includes/TitlePermissionTest.php (modified) (history)
  • /trunk/phase3/tests/phpunit/includes/UserTest.php (modified) (history)

Diff [purge]

Index: trunk/phase3/RELEASE-NOTES-1.19
@@ -29,6 +29,7 @@
3030 hooks have been removed.
3131 * New hook "Collation::factory" to allow extensions to create custom
3232 category collations.
 33+* $wgGroupPermissions now supports per namespace permissions.
3334
3435 === New features in 1.19 ===
3536 * BREAKING CHANGE: action=watch / action=unwatch now requires a token.
Index: trunk/phase3/tests/phpunit/includes/ArticleTablesTest.php
@@ -11,7 +11,7 @@
1212 $title = Title::newFromText("Bug 14404");
1313 $article = new Article( $title );
1414 $wgUser = new User();
15 - $wgUser->mRights = array( 'createpage', 'edit', 'purge' );
 15+ $wgUser->mRights['*'] = array( 'createpage', 'edit', 'purge' );
1616 $wgLanguageCode = 'es';
1717 $wgContLang = Language::factory( 'es' );
1818
Index: trunk/phase3/tests/phpunit/includes/UserTest.php
@@ -1,7 +1,11 @@
22 <?php
33
 4+define( 'NS_UNITTEST', 5600 );
 5+define( 'NS_UNITTEST_TALK', 5601 );
 6+
47 class UserTest extends MediaWikiTestCase {
58 protected $savedGroupPermissions, $savedRevokedPermissions;
 9+ protected $user;
610
711 public function setUp() {
812 parent::setUp();
@@ -10,24 +14,40 @@
1115 $this->savedRevokedPermissions = $GLOBALS['wgRevokePermissions'];
1216
1317 $this->setUpPermissionGlobals();
 18+ $this->setUpUser();
1419 }
1520 private function setUpPermissionGlobals() {
1621 global $wgGroupPermissions, $wgRevokePermissions;
1722
 23+ # Data for regular $wgGroupPermissions test
1824 $wgGroupPermissions['unittesters'] = array(
 25+ 'test' => true,
1926 'runtest' => true,
2027 'writetest' => false,
2128 'nukeworld' => false,
2229 );
2330 $wgGroupPermissions['testwriters'] = array(
 31+ 'test' => true,
2432 'writetest' => true,
2533 'modifytest' => true,
2634 );
27 -
 35+ # Data for regular $wgRevokePermissions test
2836 $wgRevokePermissions['formertesters'] = array(
2937 'runtest' => true,
3038 );
 39+
 40+ # Data for namespace based $wgGroupPermissions test
 41+ $wgGroupPermissions['unittesters']['writedocumentation'] = array(
 42+ NS_MAIN => false, NS_UNITTEST => true,
 43+ );
 44+ $wgGroupPermissions['testwriters']['writedocumentation'] = true;
 45+
3146 }
 47+ private function setUpUser() {
 48+ $this->user = new User;
 49+ $this->user->addGroup( 'unittesters' );
 50+ }
 51+
3252 public function tearDown() {
3353 parent::tearDown();
3454
@@ -55,4 +75,90 @@
5676 $this->assertNotContains( 'modifytest', $rights );
5777 $this->assertNotContains( 'nukeworld', $rights );
5878 }
 79+
 80+ public function testNamespaceGroupPermissions() {
 81+ $rights = User::getGroupPermissions( array( 'unittesters' ) );
 82+ $this->assertNotContains( 'writedocumentation', $rights );
 83+
 84+ $rights = User::getGroupPermissions( array( 'unittesters' ) , NS_MAIN );
 85+ $this->assertNotContains( 'writedocumentation', $rights );
 86+ $this->assertNotContains( 'modifytest', $rights );
 87+
 88+ $rights = User::getGroupPermissions( array( 'unittesters' ), NS_HELP );
 89+ $this->assertNotContains( 'writedocumentation', $rights );
 90+ $this->assertNotContains( 'modifytest', $rights );
 91+
 92+ $rights = User::getGroupPermissions( array( 'unittesters' ), NS_UNITTEST );
 93+ $this->assertContains( 'writedocumentation', $rights );
 94+
 95+ $rights = User::getGroupPermissions(
 96+ array( 'unittesters', 'testwriters' ), NS_MAIN );
 97+ $this->assertContains( 'writedocumentation', $rights );
 98+ }
 99+
 100+ public function testUserPermissions() {
 101+ $rights = $this->user->getRights();
 102+ $this->assertContains( 'runtest', $rights );
 103+ $this->assertNotContains( 'writetest', $rights );
 104+ $this->assertNotContains( 'modifytest', $rights );
 105+ $this->assertNotContains( 'nukeworld', $rights );
 106+ $this->assertNotContains( 'writedocumentation', $rights );
 107+
 108+ $rights = $this->user->getRights( NS_MAIN );
 109+ $this->assertNotContains( 'writedocumentation', $rights );
 110+ $this->assertNotContains( 'modifytest', $rights );
 111+
 112+ $rights = $this->user->getRights( NS_HELP );
 113+ $this->assertNotContains( 'writedocumentation', $rights );
 114+ $this->assertNotContains( 'modifytest', $rights );
 115+
 116+ $rights = $this->user->getRights( NS_UNITTEST );
 117+ $this->assertContains( 'writedocumentation', $rights );
 118+ }
 119+
 120+ /**
 121+ * @dataProvider provideGetGroupsWithPermission
 122+ */
 123+ public function testGetGroupsWithPermission( $expected, $right, $ns ) {
 124+ $result = User::getGroupsWithPermission( $right, $ns );
 125+ sort( $result );
 126+ sort( $expected );
 127+
 128+ $this->assertEquals( $expected, $result, "Groups with permission $right" .
 129+ ( is_null( $ns ) ? '' : "in namespace $ns" ) );
 130+ }
 131+ public function provideGetGroupsWithPermission() {
 132+ return array(
 133+ array(
 134+ array( 'unittesters', 'testwriters' ),
 135+ 'test',
 136+ null
 137+ ),
 138+ array(
 139+ array( 'unittesters' ),
 140+ 'runtest',
 141+ null
 142+ ),
 143+ array(
 144+ array( 'testwriters' ),
 145+ 'writetest',
 146+ null
 147+ ),
 148+ array(
 149+ array( 'testwriters' ),
 150+ 'modifytest',
 151+ null
 152+ ),
 153+ array(
 154+ array( 'testwriters' ),
 155+ 'writedocumentation',
 156+ NS_MAIN
 157+ ),
 158+ array(
 159+ array( 'unittesters', 'testwriters' ),
 160+ 'writedocumentation',
 161+ NS_UNITTEST
 162+ ),
 163+ );
 164+ }
59165 }
\ No newline at end of file
Index: trunk/phase3/tests/phpunit/includes/TitlePermissionTest.php
@@ -56,11 +56,17 @@
5757 }
5858
5959 function setUserPerm( $perm ) {
60 - if ( is_array( $perm ) ) {
61 - $this->user->mRights = $perm;
62 - } else {
63 - $this->user->mRights = array( $perm );
 60+ // Setting member variables is evil!!!
 61+
 62+ if ( !is_array( $perm ) ) {
 63+ $perm = array( $perm );
6464 }
 65+ for ($i = 0; $i < 100; $i++) {
 66+ $this->user->mRights[$i] = $perm;
 67+ }
 68+
 69+ // Hack, hack hack ...
 70+ $this->user->mRights['*'] = $perm;
6571 }
6672
6773 function setTitle( $ns, $title = "Main_Page" ) {
Index: trunk/phase3/includes/User.php
@@ -2240,16 +2240,29 @@
22412241
22422242 /**
22432243 * Get the permissions this user has.
 2244+ * @param $ns int If numeric, get permissions for this namespace
22442245 * @return Array of String permission names
22452246 */
2246 - function getRights() {
 2247+ function getRights( $ns = null ) {
 2248+ $key = is_null( $ns ) ? '*' : intval( $ns );
 2249+
22472250 if ( is_null( $this->mRights ) ) {
2248 - $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
2249 - wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
 2251+ $this->mRights = array();
 2252+ }
 2253+
 2254+ if ( !isset( $this->mRights[$key] ) ) {
 2255+ $this->mRights[$key] = self::getGroupPermissions( $this->getEffectiveGroups(), $ns );
 2256+ wfRunHooks( 'UserGetRights', array( $this, &$this->mRights[$key], $ns ) );
22502257 // Force reindexation of rights when a hook has unset one of them
2251 - $this->mRights = array_values( $this->mRights );
 2258+ $this->mRights[$key] = array_values( $this->mRights[$key] );
22522259 }
2253 - return $this->mRights;
 2260+ if ( is_null( $ns ) ) {
 2261+ return $this->mRights[$key];
 2262+ } else {
 2263+ // Merge non namespace specific rights
 2264+ return array_merge( $this->mRights[$key], $this->getRights() );
 2265+ }
 2266+
22542267 }
22552268
22562269 /**
@@ -2351,7 +2364,7 @@
23522365 }
23532366 $this->loadGroups();
23542367 $this->mGroups[] = $group;
2355 - $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
 2368+ $this->mRights = null;
23562369
23572370 $this->invalidateCache();
23582371 }
@@ -2381,7 +2394,7 @@
23822395 }
23832396 $this->loadGroups();
23842397 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2385 - $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
 2398+ $this->mRights = null;
23862399
23872400 $this->invalidateCache();
23882401 }
@@ -2434,9 +2447,10 @@
24352448 /**
24362449 * Internal mechanics of testing a permission
24372450 * @param $action String
 2451+ * @paramn $ns int Namespace optional
24382452 * @return bool
24392453 */
2440 - public function isAllowed( $action = '' ) {
 2454+ public function isAllowed( $action = '', $ns = null ) {
24412455 if ( $action === '' ) {
24422456 return true; // In the spirit of DWIM
24432457 }
@@ -2448,7 +2462,7 @@
24492463 }
24502464 # Use strict parameter to avoid matching numeric 0 accidentally inserted
24512465 # by misconfiguration: 0 == 'foo'
2452 - return in_array( $action, $this->getRights(), true );
 2466+ return in_array( $action, $this->getRights( $ns ), true );
24532467 }
24542468
24552469 /**
@@ -3397,26 +3411,51 @@
33983412 * @param $groups Array of Strings List of internal group names
33993413 * @return Array of Strings List of permission key names for given groups combined
34003414 */
3401 - static function getGroupPermissions( $groups ) {
 3415+ static function getGroupPermissions( $groups, $ns = null ) {
34023416 global $wgGroupPermissions, $wgRevokePermissions;
34033417 $rights = array();
3404 - // grant every granted permission first
 3418+
 3419+ // Grant every granted permission first
34053420 foreach( $groups as $group ) {
34063421 if( isset( $wgGroupPermissions[$group] ) ) {
3407 - $rights = array_merge( $rights,
3408 - // array_filter removes empty items
3409 - array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
 3422+ $rights = array_merge( $rights, self::extractRights(
 3423+ $wgGroupPermissions[$group], $ns ) );
34103424 }
34113425 }
3412 - // now revoke the revoked permissions
 3426+
 3427+ // Revoke the revoked permissions
34133428 foreach( $groups as $group ) {
34143429 if( isset( $wgRevokePermissions[$group] ) ) {
3415 - $rights = array_diff( $rights,
3416 - array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
 3430+ $rights = array_diff( $rights, self::extractRights(
 3431+ $wgRevokePermissions[$group], $ns ) );
34173432 }
34183433 }
34193434 return array_unique( $rights );
34203435 }
 3436+
 3437+ /**
 3438+ * Helper for User::getGroupPermissions
 3439+ * @param array $list
 3440+ * @param int $ns
 3441+ * @return array
 3442+ */
 3443+ private static function extractRights( $list, $ns ) {
 3444+ $rights = array();
 3445+ foreach( $list as $right => $value ) {
 3446+ if ( is_array( $value ) ) {
 3447+ # This is a list of namespaces where the permission applies
 3448+ if ( !is_null( $ns ) && !empty( $value[$ns] ) ) {
 3449+ $rights[] = $right;
 3450+ }
 3451+ } else {
 3452+ # This is a boolean indicating that the permission applies
 3453+ if ( $value ) {
 3454+ $rights[] = $right;
 3455+ }
 3456+ }
 3457+ }
 3458+ return $rights;
 3459+ }
34213460
34223461 /**
34233462 * Get all the groups who have a given permission
@@ -3424,11 +3463,11 @@
34253464 * @param $role String Role to check
34263465 * @return Array of Strings List of internal group names with the given permission
34273466 */
3428 - static function getGroupsWithPermission( $role ) {
 3467+ static function getGroupsWithPermission( $role, $ns = null ) {
34293468 global $wgGroupPermissions;
34303469 $allowedGroups = array();
34313470 foreach ( $wgGroupPermissions as $group => $rights ) {
3432 - if ( isset( $rights[$role] ) && $rights[$role] ) {
 3471+ if ( in_array( $role, self::getGroupPermissions( array( $group ), $ns ), true ) ) {
34333472 $allowedGroups[] = $group;
34343473 }
34353474 }
Index: trunk/phase3/includes/Title.php
@@ -1239,34 +1239,33 @@
12401240 * @return Array list of errors
12411241 */
12421242 private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
 1243+ $ns = $this->getNamespace();
 1244+
12431245 if ( $action == 'create' ) {
1244 - if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1245 - ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
 1246+ if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk', $ns ) ) ||
 1247+ ( !$this->isTalkPage() && !$user->isAllowed( 'createpage', $ns ) ) ) {
12461248 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' );
12471249 }
12481250 } elseif ( $action == 'move' ) {
1249 - if ( !$user->isAllowed( 'move-rootuserpages' )
1250 - && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
 1251+ if ( !$user->isAllowed( 'move-rootuserpages', $ns )
 1252+ && $ns == NS_USER && !$this->isSubpage() ) {
12511253 // Show user page-specific message only if the user can move other pages
12521254 $errors[] = array( 'cant-move-user-page' );
12531255 }
12541256
12551257 // Check if user is allowed to move files if it's a file
1256 - if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
 1258+ if ( $ns == NS_FILE && !$user->isAllowed( 'movefile', $ns ) ) {
12571259 $errors[] = array( 'movenotallowedfile' );
12581260 }
12591261
1260 - if ( !$user->isAllowed( 'move' ) ) {
 1262+ if ( !$user->isAllowed( 'move', $ns) ) {
12611263 // User can't move anything
1262 - global $wgGroupPermissions;
1263 - $userCanMove = false;
1264 - if ( isset( $wgGroupPermissions['user']['move'] ) ) {
1265 - $userCanMove = $wgGroupPermissions['user']['move'];
1266 - }
1267 - $autoconfirmedCanMove = false;
1268 - if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) {
1269 - $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move'];
1270 - }
 1264+
 1265+ $userCanMove = in_array( 'move', User::getGroupPermissions(
 1266+ array( 'user' ), $ns ), true );
 1267+ $autoconfirmedCanMove = in_array( 'move', User::getGroupPermissions(
 1268+ array( 'autoconfirmed' ), $ns ), true );
 1269+
12711270 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
12721271 // custom message if logged-in users without any special rights can move
12731272 $errors[] = array( 'movenologintext' );
@@ -1275,20 +1274,20 @@
12761275 }
12771276 }
12781277 } elseif ( $action == 'move-target' ) {
1279 - if ( !$user->isAllowed( 'move' ) ) {
 1278+ if ( !$user->isAllowed( 'move', $ns ) ) {
12801279 // User can't move anything
12811280 $errors[] = array( 'movenotallowed' );
1282 - } elseif ( !$user->isAllowed( 'move-rootuserpages' )
1283 - && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
 1281+ } elseif ( !$user->isAllowed( 'move-rootuserpages', $ns )
 1282+ && $ns == NS_USER && !$this->isSubpage() ) {
12841283 // Show user page-specific message only if the user can move other pages
12851284 $errors[] = array( 'cant-move-to-user-page' );
12861285 }
1287 - } elseif ( !$user->isAllowed( $action ) ) {
 1286+ } elseif ( !$user->isAllowed( $action, $ns ) ) {
12881287 // We avoid expensive display logic for quickUserCan's and such
12891288 $groups = false;
12901289 if ( !$short ) {
12911290 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
1292 - User::getGroupsWithPermission( $action ) );
 1291+ User::getGroupsWithPermission( $action, $ns ) );
12931292 }
12941293
12951294 if ( $groups ) {
@@ -1440,9 +1439,9 @@
14411440 if ( $right == 'sysop' ) {
14421441 $right = 'protect';
14431442 }
1444 - if ( $right != '' && !$user->isAllowed( $right ) ) {
 1443+ if ( $right != '' && !$user->isAllowed( $right, $this->mNamespace ) ) {
14451444 // Users with 'editprotected' permission can edit protected pages
1446 - if ( $action == 'edit' && $user->isAllowed( 'editprotected' ) ) {
 1445+ if ( $action == 'edit' && $user->isAllowed( 'editprotected', $this->mNamespace ) ) {
14471446 // Users with 'editprotected' permission cannot edit protected pages
14481447 // with cascading option turned on.
14491448 if ( $this->mCascadeRestriction ) {
@@ -1483,7 +1482,7 @@
14841483 if ( isset( $restrictions[$action] ) ) {
14851484 foreach ( $restrictions[$action] as $right ) {
14861485 $right = ( $right == 'sysop' ) ? 'protect' : $right;
1487 - if ( $right != '' && !$user->isAllowed( $right ) ) {
 1486+ if ( $right != '' && !$user->isAllowed( $right, $this->mNamespace ) ) {
14881487 $pages = '';
14891488 foreach ( $cascadingSources as $page )
14901489 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
@@ -1519,7 +1518,9 @@
15201519 if( $title_protection['pt_create_perm'] == 'sysop' ) {
15211520 $title_protection['pt_create_perm'] = 'protect'; // B/C
15221521 }
1523 - if( $title_protection['pt_create_perm'] == '' || !$user->isAllowed( $title_protection['pt_create_perm'] ) ) {
 1522+ if( $title_protection['pt_create_perm'] == '' ||
 1523+ !$user->isAllowed( $title_protection['pt_create_perm'],
 1524+ $this->mNamespace ) ) {
15241525 $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] );
15251526 }
15261527 }
Index: trunk/phase3/includes/DefaultSettings.php
@@ -3304,6 +3304,10 @@
33053305 * unable to perform certain essential tasks or access new functionality
33063306 * when new permissions are introduced and default grants established.
33073307 *
 3308+ * If set to an array instead of a boolean, it is assumed that the array is in
 3309+ * NS => bool form in order to support per-namespace permissions. Note that
 3310+ * this feature does not fully work for all permission types.
 3311+ *
33083312 * Functionality to make pages inaccessible has not been extensively tested
33093313 * for security. Use at your own risk!
33103314 *

Follow-up revisions

RevisionCommit summaryAuthorDate
r925121.18: Back out r92364, partial implementation that we don't want in 1.18 yetcatrope00:52, 19 July 2011
r92589Since r92364 UserTest.php needs a databaseplatonides21:41, 19 July 2011
r104310Follow-up r92364: Update hooks documentationbtongminh21:31, 26 November 2011
r105851Reverted r92364 (per-namespace permissions)....tstarling06:03, 12 December 2011

Comments

#Comment by Prodego (talk | contribs)   16:12, 16 July 2011

What about rights like 'read'? That wouldn't be enforced this way. What about rights that don't make sense per namespace - such as createaccount, or block? This seems like a poor idea.

#Comment by Bryan (talk | contribs)   16:15, 16 July 2011

If it doesn't make sense to sense a right per-namespace, then don't set a it per-namespace. But even if you do, this code will handle it gracefully.

For per namespace read rights the usual disclaimers apply.

#Comment by Platonides (talk | contribs)   23:41, 17 July 2011

Whoo! :)

#Comment by Aaron Schulz (talk | contribs)   19:29, 4 August 2011

I don't like how easy it is to have privilege escalation. Say you can't do action X to pages in namespace Y, but you can move pages from that namespace to something else, then one can circumvent the restrictions. I also wonder what other pitfalls exist.

#Comment by Catrope (talk | contribs)   12:23, 30 August 2011

So what happens if I want to add a namespace restriction in my extension? I can't set $wgGroupPermissions['*']['delete'][NS_BLAH] = false; because

  1. I don't know if $wgGroupPermissions['*']['delete'] is an array, it might be a boolean. But I can check for that
  2. Someone might set ['*']['delete'] = true; in LocalSettings.php, obliterating my restriction
  3. It might inadvertently disable deletes for other namespaces as well, because there is no good way to say "delete is allowed, except for this namespace"

The first two can be worked around by having extensions use userCan/getUserPermissionsErrors hooks as they currently do, but the third issue is kind of nasty: how do you specify that deleting is not OK in one specific namespace, but OK in all others?

#Comment by Catrope (talk | contribs)   12:23, 30 August 2011

#Comment by Platonides (talk | contribs)   15:27, 30 August 2011

If a extension defines a namespace that doesn't support deletion, it shouldn't be restricted with $wgGroupPermissions.

#Comment by 😂 (talk | contribs)   16:04, 3 November 2011

You changed UserGetRights, but didn't update hooks.php. Please fix.

#Comment by Bryan (talk | contribs)   21:32, 26 November 2011
#Comment by Tim Starling (talk | contribs)   04:57, 12 December 2011

I think it makes more sense to have per-namespace permissions in a separate configuration global, with $wgGroupPermissions retaining the old format and providing a default policy for non-listed namespaces. This is for the reasons Catrope gave, and also because in the future we might think of other criteria besides namespaces that we want to include in our permissions system. The proposed configuration format only supports per-namespace permissions, it doesn't support time-of-day permissions or per-category permissions or anything like that.

Bug 14801 could be solved by patching SpecialUndelete to check $this->mTitleObject->userCan('deletedhistory') etc. instead of just $wgUser->isAllowed(), and then hooking getUserPermissionsErrors in a small Wikimedia-specific extension or in a configuration file. I don't think anything grand is needed. But if we do want to do something grand, we should make sure we get it right.

#Comment by Tim Starling (talk | contribs)   06:04, 12 December 2011

See the commit message on r105851 for more thoughts on this subject.

Status & tagging log