a537d34dcc
* Demonstration of bug to test when middleware added - Adding middleware, even no-op, causes test to fail * Make middleware async, but introduce delay. Revert test back to original. * Revert tests * Add a 1ms sleep to the test to reduce likelihood of flakiness * Rate limiting on all express endpoints using middleware * Adds all configuration for existing rate limited endpoints * It is helpful to add the package to yarn first * Implements respectsLimits using Redis rate limiter * Fix for test `Should rate-limit user creation` - if rate limit error, post to `/auth/local/register` will return a 429 status code * All rate limiting provided by new ratelimiter.ts * Consolidate typescript interfaces * Amend signature of function to require source to be passed in, and not try to guess it from the request * Rename respectsLimits to isWithinRateLimits * Throw within catch of Promise * Replace rejectsRequestWithRatelimitStatusIfNeeded throughout code * Sending rate limit response should deal with other types of error - Sentry notified of the error * Express middleware rate limits by a 3 second burst or a daily rate - Provide action when generating 429 response * Prevent DOS of Redis * Add 'Retry-After' for all cases when responding with 429 status code - default of 1 day, but dynamic based on available information * Generate rate limiters once, on init - Improved and consistent handling of exit from functions - fixed environment variable names * WIP Refactor rate limiting setup Co-authored-by: Iain Sproat <iainsproat@users.noreply.github.com> * WIP: fixed references, now runs but tests fail * Use getSourceFromRequest where possible * WIP: unit tests for rate limiter * Unit tests for ratelimiter * feat(IFC): WIP IFC parser improvements * Revert "feat(IFC): WIP IFC parser improvements" This reverts commit093089a2c4. * refactor authz, rate limiting middleware to global Co-authored-by: Kristaps Fabians Geikins <fabis94@users.noreply.github.com> Co-authored-by: Iain Sproat <iainsproat@users.noreply.github.com> * invites tests fix * fix(server ratelimiter): export public interfaces * Unit test for rate limiter use in memory rate limiter - in memory rate limiter is configured with zero limit by default * Fixed #1219 (#1221) * WIP: improve auth test for rate limiting user creation * ci(circleci config): publishing was broken when main branch was tagged (i.e. for releases) (#1224) * Gitignore CPU profiles * All tests are now passing locally * Fixed an issue in the frontend which was causing the views not to work. Fixed an issue with object selection camera animation where the dolly lerp factor was much too high for smooth animation (#1225) * feat(structured logging): implements structured logging for backend (#1217) * each log line is a json object * structured logging allows logs to be ingested by machines and the logs to be indexed and queried addresses #1105 * structured logging allows arbitrary properties to be appended to each log line, and ingestion of logs to remain robust * Structured logging provided by `pino` library * Add `express-pino-logger` dependency * Remove `debug`, `morgan`, and `morgan-debug` and replace with structured logging * `console.log` & `console.error` replaced with structured logging in backend * Remove `DEBUG` environment variable and replace with `LOG_LEVEL` - Note that there is a test which reads from a logged line on `stdout`. This is not robust, it would be better to use the childProcess.pid to look up the port number. * Log errors at points we explicitly send error to Sentry * Amend indentation of a couple of log messages to align indentation with others * Revert "feat(structured logging): implements structured logging for backend (#1217)" (#1227) This reverts commit84cb74e8b3. * Move error to core/errors - augmented typescript types moved to type-augmentations * Added a missing wait in the screenshot generation loop (#1228) * refactor(server rest api): remove duplicate rate limit requests * feat(server rate limits): increase rate limits for the upload endpoints * chore(server rate limits): final cleanup Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com> Co-authored-by: Iain Sproat <iainsproat@users.noreply.github.com> Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com> Co-authored-by: Kristaps Fabians Geikins <fabis94@users.noreply.github.com> Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com> Co-authored-by: Alexandru Popovici <alexandrupopoviciioan@gmail.com>
272 lines
7.8 KiB
TypeScript
272 lines
7.8 KiB
TypeScript
import {
|
|
Scopes,
|
|
Roles,
|
|
ServerRoles,
|
|
StreamRoles
|
|
} from '@/modules/core/helpers/mainConstants'
|
|
import { getRoles } from '@/modules/shared'
|
|
import { getStream } from '@/modules/core/services/streams'
|
|
|
|
import {
|
|
BaseError,
|
|
ForbiddenError,
|
|
UnauthorizedError,
|
|
ContextError,
|
|
BadRequestError
|
|
} from '@/modules/shared/errors'
|
|
// import { getbAllRoles } from '../core/services/generic'
|
|
|
|
interface AuthResult {
|
|
authorized: boolean
|
|
}
|
|
|
|
interface AuthFailedResult extends AuthResult {
|
|
authorized: false
|
|
error: BaseError | null
|
|
fatal?: boolean
|
|
}
|
|
|
|
interface Stream {
|
|
role?: StreamRoles
|
|
isPublic: boolean
|
|
allowPublicComments: boolean
|
|
}
|
|
|
|
export interface AuthContext {
|
|
auth: boolean
|
|
userId?: string
|
|
role?: ServerRoles
|
|
token?: string
|
|
scopes?: string[]
|
|
stream?: Stream
|
|
err?: Error | BaseError
|
|
}
|
|
|
|
export interface AuthParams {
|
|
streamId?: string
|
|
}
|
|
|
|
interface AuthData {
|
|
context: AuthContext
|
|
authResult: AuthResult
|
|
params?: AuthParams
|
|
}
|
|
|
|
interface AuthFailedData extends AuthData {
|
|
authResult: AuthFailedResult
|
|
}
|
|
|
|
export const authFailed = (
|
|
context: AuthContext,
|
|
error: BaseError | null,
|
|
fatal = false
|
|
): AuthFailedData => ({
|
|
context,
|
|
authResult: { authorized: false, error, fatal }
|
|
})
|
|
|
|
export const authSuccess = (context: AuthContext): AuthData => ({
|
|
context,
|
|
authResult: { authorized: true }
|
|
})
|
|
|
|
type AvailableRoles = ServerRoles | StreamRoles
|
|
|
|
interface RoleData<T extends AvailableRoles> {
|
|
weight: number
|
|
name: T
|
|
}
|
|
|
|
export type AuthPipelineFunction = ({
|
|
context,
|
|
authResult,
|
|
params
|
|
}: AuthData) => Promise<AuthData>
|
|
|
|
export const authHasFailed = (authResult: AuthResult): authResult is AuthFailedResult =>
|
|
'error' in authResult
|
|
|
|
interface RoleValidationInput<T extends AvailableRoles> {
|
|
requiredRole: T
|
|
rolesLookup: () => Promise<RoleData<T>[]>
|
|
iddqd: T
|
|
roleGetter: (context: AuthContext) => T | null
|
|
}
|
|
|
|
// interface StreamRoleValidationInput {
|
|
// requiredRole: StreamRoles
|
|
// rolesLookup: () => Promise<StreamRoleData[]>
|
|
// iddqd: StreamRoles
|
|
// roleGetter: (AuthContext) => StreamRoles
|
|
// }
|
|
|
|
export function validateRole<T extends AvailableRoles>({
|
|
requiredRole,
|
|
rolesLookup,
|
|
iddqd,
|
|
roleGetter
|
|
}: RoleValidationInput<T>): AuthPipelineFunction {
|
|
return async ({ context, authResult }): Promise<AuthData> => {
|
|
const roles = await rolesLookup()
|
|
//having the required role doesn't rescue from authResult failure
|
|
if (authHasFailed(authResult)) return { context, authResult }
|
|
|
|
// role validation has nothing to do with auth...
|
|
//this check doesn't belong here, move it out to the auth pipeline
|
|
if (!context.auth)
|
|
return authFailed(
|
|
context,
|
|
new UnauthorizedError('Cannot validate role without auth')
|
|
)
|
|
|
|
const contextRole = roleGetter(context)
|
|
if (!contextRole)
|
|
return authFailed(
|
|
context,
|
|
new ForbiddenError('You do not have the required role')
|
|
)
|
|
|
|
const role = roles.find((r) => r.name === requiredRole)
|
|
const myRole = roles.find((r) => r.name === contextRole)
|
|
|
|
if (!role)
|
|
return authFailed(
|
|
context,
|
|
new ForbiddenError('Invalid role requirement specified')
|
|
)
|
|
if (!myRole)
|
|
return authFailed(context, new ForbiddenError('Your role is not valid'))
|
|
if (myRole.name === iddqd || myRole.weight >= role.weight)
|
|
return authSuccess(context)
|
|
return authFailed(context, new ForbiddenError('You do not have the required role'))
|
|
}
|
|
}
|
|
|
|
export const validateServerRole = ({ requiredRole }: { requiredRole: ServerRoles }) =>
|
|
validateRole({
|
|
requiredRole,
|
|
rolesLookup: getRoles,
|
|
iddqd: Roles.Server.Admin,
|
|
roleGetter: (context) => context.role || null
|
|
})
|
|
|
|
export const validateStreamRole = ({ requiredRole }: { requiredRole: StreamRoles }) =>
|
|
validateRole({
|
|
requiredRole,
|
|
rolesLookup: getRoles,
|
|
iddqd: Roles.Stream.Owner,
|
|
roleGetter: (context) => context?.stream?.role || null
|
|
})
|
|
|
|
export const validateScope =
|
|
({ requiredScope }: { requiredScope: string }): AuthPipelineFunction =>
|
|
async ({ context, authResult }) => {
|
|
// having the required role doesn't rescue from authResult failure
|
|
if (authHasFailed(authResult)) return { context, authResult }
|
|
if (!context.scopes)
|
|
return authFailed(
|
|
context,
|
|
new ForbiddenError('You do not have the required privileges.')
|
|
)
|
|
if (
|
|
context.scopes.indexOf(requiredScope) === -1 &&
|
|
context.scopes.indexOf('*') === -1
|
|
)
|
|
return authFailed(
|
|
context,
|
|
new ForbiddenError('You do not have the required privileges.')
|
|
)
|
|
return authSuccess(context)
|
|
}
|
|
|
|
type StreamGetter = (params: { streamId: string; userId?: string }) => Promise<Stream>
|
|
|
|
// this doesn't do any checks on the scopes, its sole responsibility is to add the
|
|
// stream object to the pipeline context
|
|
export const contextRequiresStream =
|
|
(streamGetter: StreamGetter): AuthPipelineFunction =>
|
|
// stream getter is an async func over { streamId, userId } returning a stream object
|
|
// IoC baby...
|
|
async ({ context, authResult, params }) => {
|
|
if (!params?.streamId)
|
|
return authFailed(
|
|
context,
|
|
new ContextError("The context doesn't have a streamId")
|
|
)
|
|
// because we're assigning to the context, it would raise if it would be null
|
|
// its probably?? safer than returning a new context
|
|
if (!context)
|
|
return authFailed(context, new ContextError('The context is not defined'))
|
|
|
|
// cause stream getter could throw, its not a safe function if we want to
|
|
// keep the pipeline rolling
|
|
try {
|
|
const stream = await streamGetter({
|
|
streamId: params.streamId,
|
|
userId: context?.userId
|
|
})
|
|
if (!stream)
|
|
return authFailed(
|
|
context,
|
|
new BadRequestError('Stream inputs are malformed'),
|
|
true
|
|
)
|
|
context.stream = stream
|
|
return { context, authResult }
|
|
} catch (err) {
|
|
// this prob needs some more detailing to not leak internal errors
|
|
const error = err as Error
|
|
return authFailed(context, new ContextError(error.message))
|
|
}
|
|
}
|
|
|
|
export const allowForRegisteredUsersOnPublicStreamsEvenWithoutRole: AuthPipelineFunction =
|
|
async ({ context, authResult }) =>
|
|
context.auth && context.stream?.isPublic
|
|
? authSuccess(context)
|
|
: { context, authResult }
|
|
|
|
export const allowForAllRegisteredUsersOnPublicStreamsWithPublicComments: AuthPipelineFunction =
|
|
async ({ context, authResult }) =>
|
|
context.auth && context.stream?.isPublic && context.stream?.allowPublicComments
|
|
? authSuccess(context)
|
|
: { context, authResult }
|
|
|
|
export const allowAnonymousUsersOnPublicStreams: AuthPipelineFunction = async ({
|
|
context,
|
|
authResult
|
|
}) => (context.stream?.isPublic ? authSuccess(context) : { context, authResult })
|
|
|
|
export const authPipelineCreator = (
|
|
steps: AuthPipelineFunction[]
|
|
): AuthPipelineFunction => {
|
|
const pipeline: AuthPipelineFunction = async ({
|
|
context,
|
|
params,
|
|
authResult = { authorized: false }
|
|
}) => {
|
|
for (const step of steps) {
|
|
;({ context, authResult } = await step({ context, authResult, params }))
|
|
if (authHasFailed(authResult) && authResult?.fatal) break
|
|
}
|
|
// validate auth result a bit...
|
|
if (authResult.authorized && authHasFailed(authResult))
|
|
throw new Error('Auth failure')
|
|
return { context, authResult }
|
|
}
|
|
return pipeline
|
|
}
|
|
|
|
export const streamWritePermissions = [
|
|
validateServerRole({ requiredRole: Roles.Server.User }),
|
|
validateScope({ requiredScope: Scopes.Streams.Write }),
|
|
contextRequiresStream(getStream as StreamGetter),
|
|
validateStreamRole({ requiredRole: Roles.Stream.Contributor })
|
|
]
|
|
export const streamReadPermissions = [
|
|
validateServerRole({ requiredRole: Roles.Server.User }),
|
|
validateScope({ requiredScope: Scopes.Streams.Read }),
|
|
contextRequiresStream(getStream as StreamGetter),
|
|
validateStreamRole({ requiredRole: Roles.Stream.Contributor })
|
|
]
|