Regional-training RestrictAccessByCategoryAndGroup.php

From regional-training

The original extension was written by Andrés Orencio Ramirez Perez[1] and has been customized by User:Ralph for finer-grain control as outlined in category:Access Control and Security in this wiki.

<?php

// History:
// 2023-11-14 Ralph Holland - check that page catagories are not private after checking if any uer groups match a pages catagories.
// 2023-06-08 Ralph Holland - include Special:Search and Special:RecentChanges in white-listing.
// 2022-09-20 Ralph Holland - include Special:Categories white-listing.
// 2022-09-20 Ralph Holland - now permit quite a few more Special: page titles.
// 2022-09-20 Ralph Holland - fixed a comparison typo after r
// 2022-09-19 Ralph Holland - made category:edit: markings inclusive while excluding users that have no corresponding marking for the edit action.
// 2022-09-18 Ralph Holland - Now User:category is not completely exclusive, other User:category can be put on page to share with another users
//                            public is not public when a private has been placed on the page or an exclusive user:category is on the page.
//                            white-listed pages cannot override an exclusive category denying access
// 2022-08-15 Ralph Holland - made pages without a category not public, made private exclusive, implemented category:User:<name> as exclusive page access

if ( !defined( 'MEDIAWIKI' ) ) {
        die( 'Not a valid entry point.' );
}

$wgExtensionCredits['parserhook'][] = array(
        'name' => 'Restrict access by category and group',
        'author' => 'Andrés Orencio Ramirez Perez & Ralph Holland',
        'url' => 'https://www.mediawiki.org/wiki/Extension:Restrict_access_by_category_and_group',
        'description' => 'Allows to restrict access to pages by users groups and page categories: See [[Security]]',
        'version' => '3.1.0-adapted-by-ralph-holland'
);

$wgHooks['userCan'][] = 'restrictAccessByCategoryAndGroup';

function debug( $a ) {
        for ($i =0; $i < 35; $i++) {
                print " ";
        }
        print "$a";
}

function restrictAccessByCategoryAndGroup( $title, $user, $action, $result ) {

        # debug("Ralph is debugging access controls ATM title=".$title." user=".$user." action=".$action."<br/>");

        global $wgGroupPermissions;
        global $wgWhitelistRead;
        global $wgLang;
        global $wgHooks;
        global $wgContLang;
        global $wgVersion;

        // strip the prefix of Talk etc off
        $posn = strpos( $title,":");
        $prefix = $posn > -1 ? substr( $title, 0, $posn ) : "";
        $page = $posn > -1 ? substr( $title, $posn+1 ) : $title;

        # RBH
        #debug( "RBH is debugging page=".$page." prefix=".$prefix." action=".$action." user=".$user."</br>" );

        //  RBH These are special pages to be white-listed
        if (strpos($title,'Special:')===0) {
                switch($title) {
                    case 'Special:Login':
                    case 'Special:Logout':
                    case 'Special:UserLogin':
                    case 'Special:UserLogout':
                    case 'Special:CreateAccount':
                    case 'Special:Badtitle':
                    case 'Special:Random':
                    case 'Special:Version':
                    case 'Special:Categories':
                    case 'Special:AllPages':
                    case 'Special:Search':              // added 2023-06-08
                    case 'Special:RecentChanges';  // added 2023-06-08
                        # debug( "*** $title permitted" );
                        return true;

                    default:
                            if ( strpos($title,"Special:WhatLinksHere")===0 ) { // || strpos($title,"Special:RecentChangesLinked")===0 ) {
                                    # debug("*** $title must be permitted");
                                    return true;
                            }
                            break;
                    }
        }

        #if ( in_array('sysop',$user->getGroups()) ) {
                # debug( "*** sysop can see all pages" );
#               return true;
#       }

        //Build up System categories from the title's categories
        $systemCategory = array();

        // get the users groups
        $userGroups = $user->getGroups();
        $hasUserCategory = false;
        $userCategoryMatched = false;

        // now check the page categories against the system groups
        $publicPage = false;
        $privatePage = false;
        $editUserMatched = false;
        $editGroupMatched = false;

        // now process page categories
        foreach ( array_change_key_case( $title->getParentCategories(), CASE_LOWER ) as $key => $value ) {
                $formatedKey = substr( $key, ( strpos( $key, ":" ) + 1 ) );

                # RBH
                #debug( "category:".$formatedKey."<br/>" );

                // check for the exclusive category public
                if ( $formatedKey=='public' ) {
                        $publicPage = true;
                }
                // check for the exclusive category private
                elseif ( $formattedKey==='private' ) {
                        // check that the user holds private group
                        if (! in_array($formatedKey,$userGroups) ) {
                                $privatePage = true;
                        }
                }
                // check for the exclusive category user:*
                elseif ( strpos( $formatedKey, 'user:' ) === 0  ) {
                        // these are special user categories e.g. [[User:Ralph]]
                        $hasUserCategory = true;
                        $name = 'user:'.strtolower($user->getName());
                        # debug( $formatedKey.' '.$name.'<-name<br/>' );
                        if ( $name && $formatedKey===$name ) {
                                # debug( '**** user permitted by user category'.$formatedKey.' '.'allowed<br/>' );
                                $userCategoryMatched = true;
                        }
                }
                elseif ( strpos( $formatedKey, 'edit:user:' ) === 0 ) {
                        // check if user is not permitted to edit
                        if ($action === 'edit') {
                                $hasEdit = true;
                                $name = 'edit:user:'.strtolower($user->getName());
                                if ($name === $formatedKey) {
                                        # debug('*** edit user matched'.$name);
                                        $editUserMatched = true;
                                }
                        }
                }
                elseif ( strpos( $formatedKey, 'edit:' ) === 0 ) {
                        //  check is user does not have groups to permit edit
                        if ($action === 'edit') {
                                $hasEdit = true;
                                $group = substr( $formatedKey, ( strpos( $formatedKey, ":" ) + 1 ) );
                                if ( in_array($group,$userGroups) ) {
                                        # debug('*** edit group matched '.$group);
                                        $editGroupMatched = true;
                                }
                        }
                }
                else {
                        // build up non-exclusive categories
                        #debug('formatedKey='.$formatedKey . ' value=' . $formatedKey);
                        $systemCategory[ $formatedKey ] = $value;
                }
        }

        // check if category:edit: marking found
        if ($hasEdit) {
                if ($editGroupMatched) {
                        # debug('*** matched a category:edit:group marking');
                        return true;
                }
                else if ($editUserMatched) {
                        # debug('*** matched a category:edit:user: markingi');
                        return true;
                }
                else {
                        # debug('--- denied user by category:edit marking');
                        return false;
                }
        }

        // check if page marked with the user:category
        if ($userCategoryMatched) {
                # debug('*** user allowed by user:category');
                return true;
        }

        // check if user is excluded by page marked private
        if ($privatePage) {
                # debug('--- denied because the page was marked private');
                return false;
        }

        // check if user is excluded by another user:category
        if ($hasUserCategory) {
                # debug('--- denied by user:category');
                return false;
        }

        // check if page was marked as public
        if ($publicPage) {
                # debug('*** permitted by public page');
                return true;
        }

        // honour the existing white-list mechanism
        if ( count( $wgWhitelistRead ) != 0 ) {
                if ( in_array( $title, $wgWhitelistRead ) ) {
                        # debug("*** white listed" );
                        return true;
                }
        }

        // check if page has any remaining categories
        if ( count( $systemCategory ) === 0 ) {
                if ( count($userGroups) > 0 ) {
                        # debug('*** logged in users can access pages without categories');
                        return true;
                }
                # RBH
                # debug('-- anonymous user cannot access pages without categories');
                return false;
        } else {
                // check remaining page categories for inclusive private permissions
                // users must be logged in to process remaining category markings
                if ( count($userGroups) != 0) {

                        // for each group permission ...
                        $hasPrivateCategories = false;
                        foreach ( $wgGroupPermissions as $key => $value ) {

                                # debug( 'group->'.$key.' '.$action.' '.$title.'<br/>' );

                                //  ... if the group permission is marked as 'private' then check ...
                                if ( isset( $wgGroupPermissions[$key]['private'] ) ) {

                                        $hasPrivateCategories = true;

                                        // ... if page is marked with a category that corresponds to the private group
                                        if ( array_key_exists( strtolower( str_replace( " ", "_", $key ) ), $systemCategory ) ) {

                                                // ... permit access if user is assigned the group
                                                if ( in_array( $key, $userGroups) ) {
                                                        # debug( '*** permitted user holds the group '.$key );
                                                        return true;
                                                }
                                        }
                                }
                        }
                        # check if any category on page is private
                        foreach ( $systemCategory as $key => $value ) {
                                #debug('****check '.$key.' val '.$value);
                                if ( isset( $wgGroupPermission[$key]['private'] ) ) {
                                        return false;
                                }
                        }
                        return true;
                        #if (!$hasPrivateCategories) {
                                # RBH
                        #               debug('*** user permitted to access a page that does not contain private categories');
                        #       return true;
                        #}
                }
        }
        # RBH
        # debug("--- user does not hold an appropriate private category and page is not marked for their access");
        return false;
}

installation

The extension is installed in the file location:

.../mediawiki/extensions/rabcg/RestrictAccessByCategoryAndGroup.php

The extension is loaded from the:

.../mediawiki/LocalSettings.php

access control checks

The access control reference data is defined in LocalSettings.php as follows:

  • privileges are denied by:
$wgGroupPermissions[<group>]['*'] = false;
  • and the private privilege for a group is defined by:
$wgGroupPermissions[<group>]['private'] = true;

Those groups may be assigned to a user account via the Special:UserRights page, where the privilege groups that were assigned are accessible via $user->getGroups() in this extension.

The Access Control checks are performed in this order as follows:

  1. the special pages:
    are always permitted.
  2. pages marked with any Categories are examined to determine if they are marked with an Access Control marking as follows:
    1. when marked with a category:user: mark (containing a user name) then the page is accessible only to:
    2. when marked with a private [[:category:]] including a group (e.g. category:lesson) then then logged-in user is permitted to access the page when that user is a member of the group (e.g. the lesson group) i.e. if the group has been assigned in to the user in the Special:UserRights pages.
    3. when marked with category:private the page is inaccessible to any user that is not a member of the category:private group, except for a sysop who is permitted to access any page
    4. when marked with a category:edit: mark the page is restricted for edit unless there is:
    5. when marked with category:public the page is visible to any user, provide none of the previous read access checks have failed to grant access.
  3. pages that are not marked with any categories are not accessible to an anonymous user.
  4. only pages marked with category:public or pages that have been white-listed may be viewed by any user, including anonymous users (those who have not logged-in).

Code changes

references

categories