Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,6 @@ limit. Codes are stored hashed and are burned after five wrong entries. Every is
failure and rate-limit hit is logged through the `MemberAccess` log channel, with the email
address hashed.

### The roster

A member's name gives nothing away, so what is left to keep is that they exist at all. The action
API query modules whose purpose is enumerating accounts are closed to the reader group, and three
logs are closed to anyone who cannot manage members: the new user log, where every member's account
creation is recorded, the block log, where every deactivation is, and the rename log, which names
both sides of a rename performed by hand. Restricting a log type also keeps it out of recent
changes. The special pages that list or resolve accounts are closed to the reader group as well, and
transcluding `Special:ListUsers` renders it empty for everyone, since what a transclusion renders is
not kept to the reader who asked for it. `Special:Redirect` is closed whole, so members lose its
other lookups too, and `Special:FilePath`, which redirects through it.

Page histories and recent changes still name whoever acted, which on a members-only wiki means the
staff who edit: members cannot appear there, since they cannot change anything.

### Login routes

Two settings, one per login route, say what the allowlist governs there and whether the code route
Expand Down Expand Up @@ -196,14 +181,11 @@ Whatever the login routes are set to, loading the extension:
sending email, reading the abuse filters and their log, and reading or changing their own private
information or preferences, which closes `Special:ChangeEmail` to them;
* sets `$wgBlockDisablesLogin`, so blocking a member keeps them out of a private wiki;
* restricts the `newusers`, `block` and `renameuser` logs to the `memberaccess-manage` right, unless
the wiki already restricted them, so that who joined, who was deactivated, and what an account
renamed by hand was called before stay out of view;
* restricts the `renameuser` log to the `memberaccess-manage` right, unless the wiki already
restricted it, since an entry there names what an account was called before, which for a member
can be their email address;
* refuses members a password, whatever the routes: setting one and having a temporary one mailed
stay refused;
* closes the account-listing API modules to the reader group;
* closes the special pages that list or resolve accounts to the reader group, and transcluding
`Special:ListUsers` to everyone.
stay refused.

While the code route is turned on, it also turns off ConfirmEdit's `badloginperuser` captcha trigger,
so failed logins no longer escalate to a captcha for the account they name, for everyone on the wiki
Expand Down Expand Up @@ -346,8 +328,6 @@ body it cannot read — carries MediaWiki's error shape rather than this one.
| `$wgMemberAccessIpDailyLimit` | int | `50` | Maximum code requests per client IP within 24 hours |
| `$wgMemberAccessSenderAddress` | ?string | `null` | Address that login codes and invitations are sent from. Falls back to `$wgPasswordSender` |
| `$wgMemberAccessSessionDurationSeconds` | int | `2592000` | How long a remembered login lasts, wiki-wide. Thirty days, against core's 180 days. `0` leaves `$wgExtendedLoginCookieExpiration` alone |
| `$wgMemberAccessBlockedApiModules` | string[] | `[ 'allusers', 'users', 'blocks' ]` | Action API query submodules the reader group may not use |
| `$wgMemberAccessBlockedSpecialPages` | string[] | `[ 'Listusers', 'Activeusers', 'BlockList', 'Redirect', 'Userrights' ]` | Special pages the reader group may not open. Canonical names; an alias does not match. Setting it adds to the shipped list rather than replacing it, so those pages cannot be dropped |

Issued codes and rate-limit counters are held in the main object stash (`$wgMainStash`), which is
database-backed by default. Point it at Redis or Valkey to keep them out of the database.
Expand Down
Binary file added docs/screenshots/code-entry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 0 additions & 16 deletions extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@
"passwordReset": {
"factory": "ProfessionalWiki\\MemberAccess\\MemberAccessExtension::newPasswordResetHookHandler"
},
"api": {
"factory": "ProfessionalWiki\\MemberAccess\\MemberAccessExtension::newUserListApiHookHandler"
},
"specialPages": {
"factory": "ProfessionalWiki\\MemberAccess\\MemberAccessExtension::newUserListSpecialPageHookHandler"
},
"loginForm": {
"class": "ProfessionalWiki\\MemberAccess\\EntryPoints\\LoginFormHandler"
}
Expand All @@ -50,8 +44,6 @@
"PluggableAuthUserAuthorization": "sso",
"UserLoggedIn": "login",
"SpecialPasswordResetOnSubmit": "passwordReset",
"ApiCheckCanExecute": "api",
"SpecialPageBeforeExecute": "specialPages",
"AuthChangeFormFields": "loginForm",
"BeforePageDisplay": "loginForm"
},
Expand Down Expand Up @@ -145,14 +137,6 @@
"MemberAccessSessionDurationSeconds": {
"value": 2592000,
"description": "How long a remembered login lasts, in seconds. Sets $wgExtendedLoginCookieExpiration while a route can log a member in, so it applies to everyone on the wiki, not only to members. Set to 0 to leave that setting alone."
},
"MemberAccessBlockedApiModules": {
"value": [ "allusers", "users", "blocks" ],
"description": "Names of action API query submodules the reader group may not use."
},
"MemberAccessBlockedSpecialPages": {
"value": [ "Listusers", "Activeusers", "BlockList", "Redirect", "Userrights" ],
"description": "Canonical names of special pages the reader group may not open."
}
},
"RestRoutes": [
Expand Down
2 changes: 0 additions & 2 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@
"memberaccess-auth-not-authorized": "This address cannot be used to log in here.",
"memberaccess-auth-failed": "Logging in did not complete. Request a new login code; if this keeps happening, contact an administrator of this wiki.",
"memberaccess-auth-password-refused": "Members log in with a one-time code, so this account cannot have a password.",
"memberaccess-api-module-denied": "The \"$1\" API module is not available to members.",
"memberaccess-special-page-denied": "This special page is not available to members.",
"memberaccess-block-reason": "Membership ended",
"memberaccess-unblock-reason": "Membership resumed",
"right-memberaccess-manage": "Manage the member allowlist and roster",
Expand Down
2 changes: 0 additions & 2 deletions i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@
"memberaccess-auth-not-authorized": "Error shown when a correct code was entered but the address is not on the allowlist.",
"memberaccess-auth-failed": "Error shown when a code login cannot continue, for any of several internal reasons. Deliberately vague, so keep the translation unspecific rather than naming a cause.",
"memberaccess-auth-password-refused": "Error shown when setting or resetting the password of a member account is attempted.",
"memberaccess-api-module-denied": "Action API error shown when a member asks for a query module that lists the wiki's accounts.\n\nParameters:\n* $1 - the name of the query submodule, for example allusers",
"memberaccess-special-page-denied": "Error page shown when a member opens a special page that lists or resolves the wiki's accounts.",
"memberaccess-block-reason": "Reason recorded in the block log when a member is deactivated. Reactivation recognises the extension's own block by this text in the wiki's content language: after a rewording, blocks placed under the old wording are reported as foreign instead of lifted.",
"memberaccess-unblock-reason": "Reason recorded in the block log when a member is reactivated.",
"right-memberaccess-manage": "{{doc-right|memberaccess-manage}}",
Expand Down
27 changes: 12 additions & 15 deletions src/EntryPoints/RegistrationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class RegistrationHandler {

private const MANAGE_RIGHT = 'memberaccess-manage';
private const PUBLIC_LOG = '*';
private const LOGS_THAT_RECORD_MEMBERS = [ 'newusers', 'block', 'renameuser' ];
private const RENAME_LOG = 'renameuser';
private const PER_ADDRESS_CAPTCHA_TRIGGER = 'badloginperuser';
private const SSO_EMAIL_PROCESSOR_SETTING = 'wgOpenIDConnect_EmailProcessor';
private const SSO_USERNAME_PROCESSOR_SETTING = 'wgOpenIDConnect_PreferredUsernameProcessor';
Expand All @@ -39,7 +39,7 @@ public static function onRegistration(): void {
*/
private static function applyWhatMembersNeed(): void {
self::moveReaderRevocationsToTheConfiguredGroup();
self::closeTheLogsThatRecordMembers();
self::closeTheRenameLog();

// A deactivated member is blocked, and only this makes a block keep them out of a private wiki.
$GLOBALS['wgBlockDisablesLogin'] = true;
Expand All @@ -64,24 +64,21 @@ private static function moveReaderRevocationsToTheConfiguredGroup(): void {
}

/**
* Three core logs record members: the new user log, where every account creation is, the block
* log, where every deactivation is, and the rename log, where a rename performed by hand names
* the account on both sides of it. All are closed to everyone who cannot manage members, which
* also keeps them out of recent changes, since a restricted log type is never written there.
* A rename log entry names what an account was called before, which for a member can be their
* email address, and the entry is there for good, so the log is closed to everyone who cannot
* manage members. That also keeps it out of recent changes, since a restricted log type is never
* written there.
*
* A member's name says nothing about them, so what they give away is that somebody joined, was
* deactivated or was renamed, and when. A member still under a name from before the extension
* minted them is the one whose rename would name an address.
* Every other log names accounts under the names the wiki shows anyway, and a member's name says
* nothing about them, so this is the only log the extension closes.
*
* A wiki that restricted one of them further keeps its own setting.
* A wiki that restricted it further keeps its own setting.
*/
private static function closeTheLogsThatRecordMembers(): void {
private static function closeTheRenameLog(): void {
$restrictions = self::globalArray( 'wgLogRestrictions' );

foreach ( self::LOGS_THAT_RECORD_MEMBERS as $logType ) {
if ( ( $restrictions[$logType] ?? self::PUBLIC_LOG ) === self::PUBLIC_LOG ) {
$restrictions[$logType] = self::MANAGE_RIGHT;
}
if ( ( $restrictions[self::RENAME_LOG] ?? self::PUBLIC_LOG ) === self::PUBLIC_LOG ) {
$restrictions[self::RENAME_LOG] = self::MANAGE_RIGHT;
}

$GLOBALS['wgLogRestrictions'] = $restrictions;
Expand Down
107 changes: 0 additions & 107 deletions src/EntryPoints/UserListApiHandler.php

This file was deleted.

63 changes: 0 additions & 63 deletions src/EntryPoints/UserListSpecialPageHandler.php

This file was deleted.

42 changes: 0 additions & 42 deletions src/MemberAccessExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@
use ProfessionalWiki\MemberAccess\EntryPoints\REST\SendInvitationApi;
use ProfessionalWiki\MemberAccess\EntryPoints\Auth\SsoAuthorizationHandler;
use ProfessionalWiki\MemberAccess\EntryPoints\Auth\SsoUsernameProcessor;
use ProfessionalWiki\MemberAccess\EntryPoints\UserListApiHandler;
use ProfessionalWiki\MemberAccess\EntryPoints\UserListSpecialPageHandler;
use ProfessionalWiki\MemberAccess\Persistence\DatabaseAllowlistRepository;
use ProfessionalWiki\MemberAccess\Persistence\DatabaseMemberGroupRepository;
use ProfessionalWiki\MemberAccess\Persistence\DatabaseMemberRepository;
Expand Down Expand Up @@ -317,30 +315,6 @@ private function newCsrfTokenSet(): CsrfTokenSet {
return new CsrfTokenSet( RequestContext::getMain()->getRequest() );
}

public static function newUserListApiHookHandler(): UserListApiHandler {
return self::getInstance()->newUserListApiHandler();
}

private function newUserListApiHandler(): UserListApiHandler {
return new UserListApiHandler(
userGroups: MediaWikiServices::getInstance()->getUserGroupManager(),
readerGroup: $this->getReaderGroup(),
blockedModules: $this->getStringListConfig( 'MemberAccessBlockedApiModules' )
);
}

public static function newUserListSpecialPageHookHandler(): UserListSpecialPageHandler {
return self::getInstance()->newUserListSpecialPageHandler();
}

private function newUserListSpecialPageHandler(): UserListSpecialPageHandler {
return new UserListSpecialPageHandler(
userGroups: MediaWikiServices::getInstance()->getUserGroupManager(),
readerGroup: $this->getReaderGroup(),
blockedPages: $this->getStringListConfig( 'MemberAccessBlockedSpecialPages' )
);
}

public static function newMemberLoginHookHandler(): MemberLoginHandler {
return self::getInstance()->newMemberLoginHandler();
}
Expand Down Expand Up @@ -621,22 +595,6 @@ private function getIntConfig( string $name ): int {
return is_scalar( $value ) ? intval( $value ) : 0;
}

/**
* @return string[]
*/
private function getStringListConfig( string $name ): array {
$value = $this->getConfigValue( $name );
$names = [];

foreach ( is_array( $value ) ? $value : [] as $entry ) {
if ( is_string( $entry ) ) {
$names[] = $entry;
}
}

return $names;
}

private function getConfigValue( string $name ): mixed {
return MediaWikiServices::getInstance()->getMainConfig()->get( $name );
}
Expand Down
Loading
Loading