NOTE: TDF Template: https://docs.google.com/document/d/1RT3mWt57RkGJdeV5kVH_eoVOBu-97w7sYheepKt6DgM/edit#
Problem:
Some key functionality attached to user relies on global state to gain access to the current user's session as well as HTTP request information such as the client's IP address and cookies. This is needed in particular for abuse prevention via auto-blocking (based on IP and cookies) as well as rate limit (per user, per session, or per IP). We need a way to provide access to this information without having to rely on global state.
Examples of this can be found in PermissionManager and RequestManager, or by looking for usages of User::getRequest or User::pingLimiter. Callers of RequestContent::getMain() are also problematic, though that method is often called for other reasons as well.
## Proposal 2021 (Authority spike and TDF):
Define a new interface that represents the acting agent's authority. Objects implementing this Authority interface will be made available to code that needs to make permission checks instead of the User object, which will already be available in such code. This becomes possible in a backwards compatible way by making the User class implement the new interface.
Authority interface:
```lang=php
/**
* Returns the actor associated with this authority.
*/
public function getActor(): UserIdentity;
/**
* Checks whether this authority has the given permission in general.
* For some permissions, exceptions may exist, both positive and negative, on a per-target basis.
*/
public function isAllowed( string $permission ): bool;
/**
* Checks whether this authority can probably perform the given action on the given target page.
* This method offers a fast, lightweight check, and may produce false positives.
* It is intended for determining which UI elements should be offered to the user.
*/
public function probablyCan( string $action, PageIdentity $target, PermissionStatus $status = null ): bool;
/**
* Checks whether this authority can perform the given action on the given target page.
* This method performs a thorough check, but does not protect against race conditions.
*/
public function definitelyCan( string $action, PageIdentity $target, PermissionStatus $status = null ): bool;
/**
* Authorize read access. This should be used immediately before performing read access on
* restricted information.
*
* Calling this method may have non-trivial side-effects, such as incrementing a rate limit
* counter.
*/
public function authorizeRead( string $action, PageIdentity $target, PermissionStatus $status = null ): bool;
/**
* Authorize write access. This should be used immediately before updating
* persisted information.
*
* Calling this method may have non-trivial side-effects, such as incrementing a rate limit
* counter.
*/
public function authorizeWrite( string $action, PageIdentity $target, PermissionStatus $status = null ): bool;
```
Multiple implementations will be created: besides having User implement Authority, we will implement the actual logic in a WebRequestAuthority class that User will delegate to. Initially, WebRequestAuthority will be implemented based on PermissionManager. But soon, this relationship will be reversed: the logic in PermissionManager will largely be moved to WebRequestAuthority, removing the need to access global state in the process. The Repsective methods in PermissionManager will at that point become deprecated stubs.
In addition, we will create an UltimateAuthority class for use in maintenance scripts, and a SimpleAuthorityClass (based on an explicit list of permissions) for use in tests and perhaps, in the future, with jobs. In contrast to user, SimpleAuthorityClass would be properly serializable for this purpose.
Some design decisions that were made:
* **User to implement Authority**: allows method signatures to be changed from expecting user to expecting Authority without breaking callers, removing the need to replace all such methods.
* **Undeprecating User::isAllowed() and friends**: this is a fortunate artifact of User to implementing Authority for the reasons above. It's important to remember that eventually, the old User class will go away, and will be replaced by using WebRequestAuthority directly. Having this method on User is a compatibility shunt.
* **Passing optional Status objects**: permission checks often only need a yes/no answer, but sometimes we want to provide the user with detailed reasons for denying a permission. We could just return a Status object, but then we would have to fully populate it every time. If we only need a yes/no answer, we can short-circuit on the first failure and return false without the need to perform more elaborate checks.
* **Introducing authorizeRead() and authorityWrite**: the `probablyCan`, `definitelyCan` and `authorize` methods correspond to the concepts of //may do//, //will do//, and //doing//. Having `authorize` separate allows us to put side-effects into the implementation, such as rate limit increments or spreading auto-blocks - even if we don't do this right away, making the distinction in the interface allows us to do it later if we want to. Separating `authorizeRead` from `authorizeWrite` allows us to avoid checks against the master database during read requests. This currenly only works "by accident": the //RIGOR_SECURE// mode will only actually hit the master DB when checking blocks, and blocks are only checks during write operations (on public wikis). A clear distinction in the interface allows this to be guaranteed by contract.
* **Adding getAuthority to IContextSource**: makes Authority available in the places where a request-based context is already available: in Special page implementations, API modules, etc. With Authority, IContextSource is exposing a much more lightweight interface, which will cover most if not all use cases currently covered by IContextSource::getUser().
## Proposal 2020 (Authority):
Define a new kind of object that represents the acting agent's authority. That object would encapsulate the access to any necessary information and state, and would provide methods for authorizing actions. That is, instead of having PermissionManager act on a value object like UserIdentity (or ActingUser, see below), the Authority object would internally access the PermissionManager, and any other services needed.
Rough draft of the Authority interface:
```lang=php
interface Authority {
function authorizeAction( $action );
function authorizeActionOn( $action, $page );
function getAuthorizationErrors( $action, $rigor );
function getAuthorizationErrorsOn( $action, $page, $rigor );
}
```
The `getAuthorizationErrors` methods check authorization, which may or may not include checks for IP blocks and cascading protection, depending on $rigor. They are idempotent. The `authorizeAction` methods throw an exception upon failure, and may modify state - in particular, they can update counters and enforce rate limits.
We would create multiple implementations of this interface, likely including one that fails all authorization as well as one that allows all authorization. But most importantly, there would be one that covers the logic currently implemented in `PermissionManager::userCan` and friends: it would be instantiated based on the current request, session, and user identity, and would perform checks based on the user group, IP blocks, rate limits, etc.
The corresponding methods in PermissionManager would be deprecated and delegate to the new class. PermissionManager would only retain logic about group based permissions, not about blocks or other session based checks.
The User class would implement the Authority interface as well, delegating to the session based Authority instance if the User object is the user for the session. `getAuthorizationErrors` could still function for a user that isn't the acting user, but `authorizeAction` would always fail for a user based Authority that doesn't correspond to the current request.
Jobs can use an implementation of Authority that is based on the user identity and checks blocks, but does not use session or IP based information.
Since User implements the new interface, the new functionality becomes available everywhere immediately. Type hints against user can be narrowed to Authority when and where possible, allowing for alternative light weight implementations of the interface to be used.
## Original Proposal (ActingUser):
Introduce ActingUser as an interface that extends UserIdentity. Critical checks, such as calls to PermissionManager::userCan with RIGOR_SECURE, must fail if presented with a UserIdentity that is not an ActingUser, or should require an ActingUser in the signature. The ActingUser represents not a user as such, but a user acting withing a certain context (a device, a session, oath token, address, etc). This context represents the execution context of the entire application (thanks th PHP's per-request execution model). For maintenance scripts and async jobs, the execution context would essentially just be "CLI" or "JOB".
Code that needs to check for context-based blocks or limits (session, IP address, cookies, device) will need access to some aspects of the http request. ActingUser could provide methods to access this information, but it's unclear how exactly these methods should behave in a non-web execution context. The relevant calling code will probably have to know about web-mode and cli-mode ActingUsers in any case.
Code that makes direct use of ActingUsers should be rare. However, it's likely to be used via many code paths for many use cases.
NOTE: this supersedes T218555