feat: Adds route-level role filtering. (#3734)

* feat: Adds route-level role filtering. Another layer in the onion of security and performance

* fix: Regression in authentication middleware
This commit is contained in:
Tom Moor
2022-07-05 21:26:49 +02:00
committed by GitHub
parent c6fdffba77
commit 831df67358
12 changed files with 539 additions and 359 deletions

View File

@@ -1,15 +1,28 @@
import { Next } from "koa";
import Logger from "@server/logging/Logger";
import tracer, { APM } from "@server/logging/tracing";
import { User, Team, ApiKey } from "@server/models";
import { getUserForJWT } from "@server/utils/jwt";
import { AuthenticationError, UserSuspendedError } from "../errors";
import { ContextWithState } from "../types";
import {
AuthenticationError,
AuthorizationError,
UserSuspendedError,
} from "../errors";
import { ContextWithState, AuthenticationTypes } from "../types";
export default function auth(
options: {
required?: boolean;
} = {}
) {
type AuthenticationOptions = {
/* An admin user role is required to access the route */
admin?: boolean;
/* A member or admin user role is required to access the route */
member?: boolean;
/**
* Authentication is parsed, but optional. Note that if a token is provided
* in the request it must be valid or the requst will be rejected.
*/
optional?: boolean;
};
export default function auth(options: AuthenticationOptions = {}) {
return async function authMiddleware(ctx: ContextWithState, next: Next) {
let token;
const authorizationHeader = ctx.request.get("authorization");
@@ -29,8 +42,11 @@ export default function auth(
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
);
}
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
} else if (ctx.body && ctx.body.token) {
} else if (
ctx.body &&
typeof ctx.body === "object" &&
"token" in ctx.body
) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
token = ctx.body.token;
} else if (ctx.request.query.token) {
@@ -39,15 +55,15 @@ export default function auth(
token = ctx.cookies.get("accessToken");
}
if (!token && options.required !== false) {
if (!token && options.optional !== true) {
throw AuthenticationError("Authentication required");
}
let user;
let user: User | null | undefined;
if (token) {
if (String(token).match(/^[\w]{38}$/)) {
ctx.state.authType = "api";
ctx.state.authType = AuthenticationTypes.API;
let apiKey;
try {
@@ -78,7 +94,7 @@ export default function auth(
throw AuthenticationError("Invalid API key");
}
} else {
ctx.state.authType = "app";
ctx.state.authType = AuthenticationTypes.APP;
user = await getUserForJWT(String(token));
}
@@ -94,8 +110,23 @@ export default function auth(
});
}
if (options.admin) {
if (!user.isAdmin) {
throw AuthorizationError("Admin role required");
}
}
if (options.member) {
if (user.isViewer) {
throw AuthorizationError("Member role required");
}
}
// not awaiting the promise here so that the request is not blocked
user.updateActiveAt(ctx.request.ip);
user.updateActiveAt(ctx.request.ip).catch((err) => {
Logger.error("Failed to update user activeAt", err);
});
ctx.state.token = String(token);
ctx.state.user = user;