mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-25 09:56:15 -04:00
webaccess: Move pre-authn authz check to a separate hook
Before this change, the authorize hook was invoked twice: once before authentication and again after (if settings.requireAuthorization is true). Now pre-authentication authorization is instead handled by a new preAuthorize hook, and the authorize hook is only invoked after the user has authenticated. Rationale: Without this change it is too easy to write an authorization plugin that is too permissive. Specifically: * If the plugin does not check the path for /admin then a non-admin user might be able to access /admin pages. * If the plugin assumes that the user has already been authenticated by the time the authorize function is called then unauthenticated users might be able to gain access to restricted resources. This change also avoids calling the plugin's authorize function twice per access, which makes it easier for plugin authors to write an authorization plugin that is easy to understand. This change may break existing authorization plugins: After this change, the authorize hook will no longer be able to authorize non-admin access to /admin pages. This is intentional. Access to admin pages should instead be controlled via the `is_admin` user setting, which can be set in the config file or by an authentication plugin. Also: * Add tests for the authenticate and authorize hooks. * Disable the authentication failure delay when testing.
This commit is contained in:
parent
a51132d712
commit
304318b618
5 changed files with 422 additions and 76 deletions
|
@ -212,6 +212,50 @@ Things in context:
|
|||
|
||||
I have no idea what this is useful for, someone else will have to add this description.
|
||||
|
||||
## preAuthorize
|
||||
Called from: src/node/hooks/express/webaccess.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. req - the request object
|
||||
2. res - the response object
|
||||
3. next - bypass callback. If this is called instead of the normal callback then
|
||||
all remaining access checks are skipped.
|
||||
|
||||
This hook is called for each HTTP request before any authentication checks are
|
||||
performed. Example uses:
|
||||
|
||||
* Always grant access to static content.
|
||||
* Process an OAuth callback.
|
||||
* Drop requests from IP addresses that have failed N authentication checks
|
||||
within the past X minutes.
|
||||
|
||||
A preAuthorize function is always called for each request unless a preAuthorize
|
||||
function from another plugin (if any) has already explicitly granted or denied
|
||||
the request.
|
||||
|
||||
You can pass the following values to the provided callback:
|
||||
|
||||
* `[]` defers the access decision to the normal authentication and authorization
|
||||
checks (or to a preAuthorize function from another plugin, if one exists).
|
||||
* `[true]` immediately grants access to the requested resource, unless the
|
||||
request is for an `/admin` page in which case it is treated the same as `[]`.
|
||||
(This prevents buggy plugins from accidentally granting admin access to the
|
||||
general public.)
|
||||
* `[false]` immediately denies the request. The preAuthnFailure hook will be
|
||||
called to handle the failure.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
exports.preAuthorize = (hookName, context, cb) => {
|
||||
if (ipAddressIsFirewalled(context.req)) return cb([false]);
|
||||
if (requestIsForStaticContent(context.req)) return cb([true]);
|
||||
if (requestIsForOAuthCallback(context.req)) return cb([true]);
|
||||
return cb([]);
|
||||
};
|
||||
```
|
||||
|
||||
## authorize
|
||||
Called from: src/node/hooks/express/webaccess.js
|
||||
|
||||
|
@ -225,47 +269,23 @@ Things in context:
|
|||
This hook is called to handle authorization. It is especially useful for
|
||||
controlling access to specific paths.
|
||||
|
||||
A plugin's authorize function is typically called twice for each access: once
|
||||
before authentication and again after. Specifically, it is called if all of the
|
||||
following are true:
|
||||
A plugin's authorize function is only called if all of the following are true:
|
||||
|
||||
* The request is not for static content or an API endpoint. (Requests for static
|
||||
content and API endpoints are always authorized, even if unauthenticated.)
|
||||
* Either authentication has not yet been performed (`context.req.session.user`
|
||||
is undefined) or the user has successfully authenticated
|
||||
(`context.req.session.user` is an object containing user-specific settings).
|
||||
* If the user has successfully authenticated, the user is not an admin. (Admin
|
||||
users are always authorized.)
|
||||
* Either the request is for an `/admin` page or the `requireAuthentication`
|
||||
setting is true.
|
||||
* Either the request is for an `/admin` page, or the user has not yet
|
||||
authenticated, or the user has authenticated and the `requireAuthorization`
|
||||
setting is true.
|
||||
* For pre-authentication invocations of a plugin's authorize function
|
||||
(`context.req.session.user` is undefined), an authorize function from a
|
||||
different plugin has not already caused the pre-authentication authorization
|
||||
to pass or fail.
|
||||
* For post-authentication invocations of a plugin's authorize function
|
||||
(`context.req.session.user` is an object), an authorize function from a
|
||||
different plugin has not already caused the post-authentication authorization
|
||||
to pass or fail.
|
||||
* The `requireAuthentication` and `requireAuthorization` settings are both true.
|
||||
* The user has already successfully authenticated.
|
||||
* The user is not an admin (admin users are always authorized).
|
||||
* The path being accessed is not an `/admin` path (`/admin` paths can only be
|
||||
accessed by admin users, and admin users are always authorized).
|
||||
* An authorize function from a different plugin has not already caused
|
||||
authorization to pass or fail.
|
||||
|
||||
For pre-authentication invocations of your authorize function, you can pass the
|
||||
following values to the provided callback:
|
||||
Note that the authorize hook cannot grant access to `/admin` pages. If admin
|
||||
access is desired, the `is_admin` user setting must be set to true. This can be
|
||||
set in the settings file or by the authenticate hook.
|
||||
|
||||
* `[true]`, `['create']`, or `['modify']` will immediately grant access without
|
||||
requiring the user to authenticate.
|
||||
* `[false]` will trigger authentication unless authentication is not required.
|
||||
* `[]` or `undefined` will defer the decision to the next authorization plugin
|
||||
(if any, otherwise it is the same as calling with `[false]`).
|
||||
|
||||
**WARNING:** Your authorize function can be called for an `/admin` page even if
|
||||
the user has not yet authenticated. It is your responsibility to fail or defer
|
||||
authorization if you do not want to grant admin privileges to the general
|
||||
public.
|
||||
|
||||
For post-authentication invocations of your authorize function, you can pass the
|
||||
following values to the provided callback:
|
||||
You can pass the following values to the provided callback:
|
||||
|
||||
* `[true]` or `['create']` will grant access to modify or create the pad if the
|
||||
request is for a pad, otherwise access is simply granted. (Access will be
|
||||
|
@ -281,11 +301,6 @@ Example:
|
|||
```
|
||||
exports.authorize = (hookName, context, cb) => {
|
||||
const user = context.req.session.user;
|
||||
if (!user) {
|
||||
// The user has not yet authenticated so defer the pre-authentication
|
||||
// authorization decision to the next plugin.
|
||||
return cb([]);
|
||||
}
|
||||
const path = context.req.path; // or context.resource
|
||||
if (isExplicitlyProhibited(user, path)) return cb([false]);
|
||||
if (isExplicitlyAllowed(user, path)) return cb([true]);
|
||||
|
@ -395,6 +410,35 @@ exports.authFailure = (hookName, context, cb) => {
|
|||
};
|
||||
```
|
||||
|
||||
## preAuthzFailure
|
||||
Called from: src/node/hooks/express/webaccess.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. req - the request object
|
||||
2. res - the response object
|
||||
|
||||
This hook is called to handle a pre-authentication authorization failure.
|
||||
|
||||
A plugin's preAuthzFailure function is only called if the pre-authentication
|
||||
authorization failure was not already handled by a preAuthzFailure function from
|
||||
another plugin.
|
||||
|
||||
Calling the provided callback with `[true]` tells Etherpad that the failure was
|
||||
handled and no further error handling is required. Calling the callback with
|
||||
`[]` or `undefined` defers error handling to a preAuthzFailure function from
|
||||
another plugin (if any, otherwise fall back to a generic 403 error page).
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
exports.preAuthzFailure = (hookName, context, cb) => {
|
||||
if (notApplicableToThisPlugin(context)) return cb([]);
|
||||
context.res.status(403).send(renderFancy403Page(context.req));
|
||||
return cb([true]);
|
||||
};
|
||||
```
|
||||
|
||||
## authnFailure
|
||||
Called from: src/node/hooks/express/webaccess.js
|
||||
|
||||
|
@ -435,7 +479,7 @@ Things in context:
|
|||
1. req - the request object
|
||||
2. res - the response object
|
||||
|
||||
This hook is called to handle an authorization failure.
|
||||
This hook is called to handle a post-authentication authorization failure.
|
||||
|
||||
A plugin's authzFailure function is only called if the authorization failure was
|
||||
not already handled by an authzFailure function from another plugin.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue