mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
refactor(core): Setup decorator based RBAC (no-changelog) (#5787)
This commit is contained in:
parent
feb2ba09b9
commit
1eeadc6114
|
@ -196,30 +196,6 @@ export async function getUserById(userId: string): Promise<User> {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a URL contains an auth-excluded endpoint.
|
|
||||||
*/
|
|
||||||
export function isAuthExcluded(url: string, ignoredEndpoints: Readonly<string[]>): boolean {
|
|
||||||
return !!ignoredEndpoints
|
|
||||||
.filter(Boolean) // skip empty paths
|
|
||||||
.find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the endpoint is `POST /users/:id`.
|
|
||||||
*/
|
|
||||||
export function isPostUsersId(req: express.Request, restEndpoint: string): boolean {
|
|
||||||
return (
|
|
||||||
req.method === 'POST' &&
|
|
||||||
new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url) &&
|
|
||||||
!req.url.includes('reinvite')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAuthenticatedRequest(request: express.Request): request is AuthenticatedRequest {
|
|
||||||
return request.user !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// hashing
|
// hashing
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { Get, Post, RestController } from '@/decorators';
|
import { Authorized, Get, Post, RestController } from '@/decorators';
|
||||||
import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper';
|
import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper';
|
||||||
import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper';
|
import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper';
|
||||||
import { issueCookie, resolveJwt } from '@/auth/jwt';
|
import { issueCookie, resolveJwt } from '@/auth/jwt';
|
||||||
|
@ -58,7 +58,6 @@ export class AuthController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log in a user.
|
* Log in a user.
|
||||||
* Authless endpoint.
|
|
||||||
*/
|
*/
|
||||||
@Post('/login')
|
@Post('/login')
|
||||||
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
||||||
|
@ -135,7 +134,6 @@ export class AuthController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate invite token to enable invitee to set up their account.
|
* Validate invite token to enable invitee to set up their account.
|
||||||
* Authless endpoint.
|
|
||||||
*/
|
*/
|
||||||
@Get('/resolve-signup-token')
|
@Get('/resolve-signup-token')
|
||||||
async resolveSignupToken(req: UserRequest.ResolveSignUp) {
|
async resolveSignupToken(req: UserRequest.ResolveSignUp) {
|
||||||
|
@ -196,8 +194,8 @@ export class AuthController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log out a user.
|
* Log out a user.
|
||||||
* Authless endpoint.
|
|
||||||
*/
|
*/
|
||||||
|
@Authorized()
|
||||||
@Post('/logout')
|
@Post('/logout')
|
||||||
logout(req: Request, res: Response) {
|
logout(req: Request, res: Response) {
|
||||||
res.clearCookie(AUTH_COOKIE_NAME);
|
res.clearCookie(AUTH_COOKIE_NAME);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import { Get, Post, Put, RestController } from '@/decorators';
|
import { Authorized, Get, Post, Put, RestController } from '@/decorators';
|
||||||
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
|
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
|
||||||
import { LdapService } from '@/Ldap/LdapService.ee';
|
import { LdapService } from '@/Ldap/LdapService.ee';
|
||||||
import { LdapSync } from '@/Ldap/LdapSync.ee';
|
import { LdapSync } from '@/Ldap/LdapSync.ee';
|
||||||
|
@ -8,6 +8,7 @@ import { BadRequestError } from '@/ResponseHelper';
|
||||||
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
|
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
|
||||||
|
@Authorized(['global', 'owner'])
|
||||||
@RestController('/ldap')
|
@RestController('/ldap')
|
||||||
export class LdapController {
|
export class LdapController {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { Delete, Get, Patch, Post, RestController } from '@/decorators';
|
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
compareHash,
|
compareHash,
|
||||||
hashPassword,
|
hashPassword,
|
||||||
|
@ -30,6 +30,7 @@ import { randomBytes } from 'crypto';
|
||||||
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
||||||
import { UserService } from '@/user/user.service';
|
import { UserService } from '@/user/user.service';
|
||||||
|
|
||||||
|
@Authorized()
|
||||||
@RestController('/me')
|
@RestController('/me')
|
||||||
export class MeController {
|
export class MeController {
|
||||||
private readonly logger: ILogger;
|
private readonly logger: ILogger;
|
||||||
|
|
|
@ -2,11 +2,12 @@ import { readFile } from 'fs/promises';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
||||||
import { Post, RestController } from '@/decorators';
|
import { Authorized, Post, RestController } from '@/decorators';
|
||||||
import { getNodeTranslationPath } from '@/TranslationHelpers';
|
import { getNodeTranslationPath } from '@/TranslationHelpers';
|
||||||
import type { Config } from '@/config';
|
import type { Config } from '@/config';
|
||||||
import type { NodeTypes } from '@/NodeTypes';
|
import type { NodeTypes } from '@/NodeTypes';
|
||||||
|
|
||||||
|
@Authorized()
|
||||||
@RestController('/node-types')
|
@RestController('/node-types')
|
||||||
export class NodeTypesController {
|
export class NodeTypesController {
|
||||||
private readonly config: Config;
|
private readonly config: Config;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
STARTER_TEMPLATE_NAME,
|
STARTER_TEMPLATE_NAME,
|
||||||
UNKNOWN_FAILURE_REASON,
|
UNKNOWN_FAILURE_REASON,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
||||||
import { NodeRequest } from '@/requests';
|
import { NodeRequest } from '@/requests';
|
||||||
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
||||||
import {
|
import {
|
||||||
|
@ -30,10 +30,10 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { Config } from '@/config';
|
import { Config } from '@/config';
|
||||||
import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';
|
|
||||||
|
|
||||||
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
||||||
|
|
||||||
|
@Authorized(['global', 'owner'])
|
||||||
@RestController('/nodes')
|
@RestController('/nodes')
|
||||||
export class NodesController {
|
export class NodesController {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -43,14 +43,6 @@ export class NodesController {
|
||||||
private internalHooks: InternalHooks,
|
private internalHooks: InternalHooks,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// TODO: move this into a new decorator `@Authorized`
|
|
||||||
@Middleware()
|
|
||||||
checkIfOwner(req: Request, res: Response, next: NextFunction) {
|
|
||||||
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner')
|
|
||||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
|
||||||
else next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
||||||
@Middleware()
|
@Middleware()
|
||||||
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
|
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { validateEntity } from '@/GenericHelpers';
|
import { validateEntity } from '@/GenericHelpers';
|
||||||
import { Get, Post, RestController } from '@/decorators';
|
import { Authorized, Get, Post, RestController } from '@/decorators';
|
||||||
import { BadRequestError } from '@/ResponseHelper';
|
import { BadRequestError } from '@/ResponseHelper';
|
||||||
import {
|
import {
|
||||||
hashPassword,
|
hashPassword,
|
||||||
|
@ -20,6 +20,7 @@ import type {
|
||||||
WorkflowRepository,
|
WorkflowRepository,
|
||||||
} from '@db/repositories';
|
} from '@db/repositories';
|
||||||
|
|
||||||
|
@Authorized(['global', 'owner'])
|
||||||
@RestController('/owner')
|
@RestController('/owner')
|
||||||
export class OwnerController {
|
export class OwnerController {
|
||||||
private readonly config: Config;
|
private readonly config: Config;
|
||||||
|
|
|
@ -65,7 +65,6 @@ export class PasswordResetController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a password reset email.
|
* Send a password reset email.
|
||||||
* Authless endpoint.
|
|
||||||
*/
|
*/
|
||||||
@Post('/forgot-password')
|
@Post('/forgot-password')
|
||||||
async forgotPassword(req: PasswordResetRequest.Email) {
|
async forgotPassword(req: PasswordResetRequest.Email) {
|
||||||
|
@ -171,7 +170,6 @@ export class PasswordResetController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify password reset token and user ID.
|
* Verify password reset token and user ID.
|
||||||
* Authless endpoint.
|
|
||||||
*/
|
*/
|
||||||
@Get('/resolve-password-token')
|
@Get('/resolve-password-token')
|
||||||
async resolvePasswordToken(req: PasswordResetRequest.Credentials) {
|
async resolvePasswordToken(req: PasswordResetRequest.Credentials) {
|
||||||
|
@ -213,7 +211,6 @@ export class PasswordResetController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify password reset token and user ID and update password.
|
* Verify password reset token and user ID and update password.
|
||||||
* Authless endpoint.
|
|
||||||
*/
|
*/
|
||||||
@Post('/change-password')
|
@Post('/change-password')
|
||||||
async changePassword(req: PasswordResetRequest.NewPassword, res: Response) {
|
async changePassword(req: PasswordResetRequest.NewPassword, res: Response) {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import type { Config } from '@/config';
|
import type { Config } from '@/config';
|
||||||
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
||||||
import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces';
|
import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces';
|
||||||
import { TagEntity } from '@db/entities/TagEntity';
|
import { TagEntity } from '@db/entities/TagEntity';
|
||||||
import type { TagRepository } from '@db/repositories';
|
import type { TagRepository } from '@db/repositories';
|
||||||
import { validateEntity } from '@/GenericHelpers';
|
import { validateEntity } from '@/GenericHelpers';
|
||||||
import { BadRequestError, UnauthorizedError } from '@/ResponseHelper';
|
import { BadRequestError } from '@/ResponseHelper';
|
||||||
import { TagsRequest } from '@/requests';
|
import { TagsRequest } from '@/requests';
|
||||||
|
|
||||||
|
@Authorized()
|
||||||
@RestController('/tags')
|
@RestController('/tags')
|
||||||
export class TagsController {
|
export class TagsController {
|
||||||
private config: Config;
|
private config: Config;
|
||||||
|
@ -91,15 +92,9 @@ export class TagsController {
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized(['global', 'owner'])
|
||||||
@Delete('/:id(\\d+)')
|
@Delete('/:id(\\d+)')
|
||||||
async deleteTag(req: TagsRequest.Delete) {
|
async deleteTag(req: TagsRequest.Delete) {
|
||||||
const isInstanceOwnerSetUp = this.config.getEnv('userManagement.isInstanceOwnerSetUp');
|
|
||||||
if (isInstanceOwnerSetUp && req.user.globalRole.name !== 'owner') {
|
|
||||||
throw new UnauthorizedError(
|
|
||||||
'You are not allowed to perform this action',
|
|
||||||
'Only owners can remove tags',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
await this.externalHooks.run('tag.beforeDelete', [id]);
|
await this.externalHooks.run('tag.beforeDelete', [id]);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { Request } from 'express';
|
||||||
import { ICredentialTypes } from 'n8n-workflow';
|
import { ICredentialTypes } from 'n8n-workflow';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { access } from 'fs/promises';
|
import { access } from 'fs/promises';
|
||||||
import { Get, RestController } from '@/decorators';
|
import { Authorized, Get, RestController } from '@/decorators';
|
||||||
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
||||||
import { Config } from '@/config';
|
import { Config } from '@/config';
|
||||||
import { NODES_BASE_DIR } from '@/constants';
|
import { NODES_BASE_DIR } from '@/constants';
|
||||||
|
@ -14,6 +14,7 @@ export declare namespace TranslationRequest {
|
||||||
export type Credential = Request<{}, {}, {}, { credentialType: string }>;
|
export type Credential = Request<{}, {}, {}, { credentialType: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized()
|
||||||
@RestController('/')
|
@RestController('/')
|
||||||
export class TranslationController {
|
export class TranslationController {
|
||||||
constructor(private config: Config, private credentialTypes: ICredentialTypes) {}
|
constructor(private config: Config, private credentialTypes: ICredentialTypes) {}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||||
import { Delete, Get, Post, RestController } from '@/decorators';
|
import { Authorized, NoAuthRequired, Delete, Get, Post, RestController } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
addInviteLinkToUser,
|
addInviteLinkToUser,
|
||||||
generateUserInviteUrl,
|
generateUserInviteUrl,
|
||||||
|
@ -41,6 +41,7 @@ import type {
|
||||||
UserRepository,
|
UserRepository,
|
||||||
} from '@db/repositories';
|
} from '@db/repositories';
|
||||||
|
|
||||||
|
@Authorized(['global', 'owner'])
|
||||||
@RestController('/users')
|
@RestController('/users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
private config: Config;
|
private config: Config;
|
||||||
|
@ -282,6 +283,7 @@ export class UsersController {
|
||||||
/**
|
/**
|
||||||
* Fill out user shell with first name, last name, and password.
|
* Fill out user shell with first name, last name, and password.
|
||||||
*/
|
*/
|
||||||
|
@NoAuthRequired()
|
||||||
@Post('/:id')
|
@Post('/:id')
|
||||||
async updateUser(req: UserRequest.Update, res: Response) {
|
async updateUser(req: UserRequest.Update, res: Response) {
|
||||||
const { id: inviteeId } = req.params;
|
const { id: inviteeId } = req.params;
|
||||||
|
@ -343,6 +345,7 @@ export class UsersController {
|
||||||
return withFeatureFlags(this.postHog, sanitizeUser(updatedUser));
|
return withFeatureFlags(this.postHog, sanitizeUser(updatedUser));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized('any')
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async listUsers(req: UserRequest.List) {
|
async listUsers(req: UserRequest.List) {
|
||||||
const users = await this.userRepository.find({ relations: ['globalRole', 'authIdentities'] });
|
const users = await this.userRepository.find({ relations: ['globalRole', 'authIdentities'] });
|
||||||
|
|
16
packages/cli/src/decorators/Authorized.ts
Normal file
16
packages/cli/src/decorators/Authorized.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import { CONTROLLER_AUTH_ROLES } from './constants';
|
||||||
|
import type { AuthRoleMetadata } from './types';
|
||||||
|
|
||||||
|
export function Authorized(authRole: AuthRoleMetadata[string] = 'any'): Function {
|
||||||
|
return function (target: Function | Object, handlerName?: string) {
|
||||||
|
const controllerClass = handlerName ? target.constructor : target;
|
||||||
|
const authRoles = (Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) ??
|
||||||
|
{}) as AuthRoleMetadata;
|
||||||
|
authRoles[handlerName ?? '*'] = authRole;
|
||||||
|
Reflect.defineMetadata(CONTROLLER_AUTH_ROLES, authRoles, controllerClass);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoAuthRequired = () => Authorized('none');
|
|
@ -1,3 +1,4 @@
|
||||||
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
||||||
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
||||||
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
||||||
|
export const CONTROLLER_AUTH_ROLES = 'CONTROLLER_AUTH_ROLES';
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export { Authorized, NoAuthRequired } from './Authorized';
|
||||||
export { RestController } from './RestController';
|
export { RestController } from './RestController';
|
||||||
export { Get, Post, Put, Patch, Delete } from './Route';
|
export { Get, Post, Put, Patch, Delete } from './Route';
|
||||||
export { Middleware } from './Middleware';
|
export { Middleware } from './Middleware';
|
||||||
|
|
|
@ -1,10 +1,36 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import type { Config } from '@/config';
|
|
||||||
import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES, CONTROLLER_ROUTES } from './constants';
|
|
||||||
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
|
||||||
import type { Application, Request, Response, RequestHandler } from 'express';
|
import type { Application, Request, Response, RequestHandler } from 'express';
|
||||||
import type { Controller, MiddlewareMetadata, RouteMetadata } from './types';
|
import type { Config } from '@/config';
|
||||||
|
import type { AuthenticatedRequest } from '@/requests';
|
||||||
|
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
||||||
|
import {
|
||||||
|
CONTROLLER_AUTH_ROLES,
|
||||||
|
CONTROLLER_BASE_PATH,
|
||||||
|
CONTROLLER_MIDDLEWARES,
|
||||||
|
CONTROLLER_ROUTES,
|
||||||
|
} from './constants';
|
||||||
|
import type {
|
||||||
|
AuthRole,
|
||||||
|
AuthRoleMetadata,
|
||||||
|
Controller,
|
||||||
|
MiddlewareMetadata,
|
||||||
|
RouteMetadata,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const createAuthMiddleware =
|
||||||
|
(authRole: AuthRole): RequestHandler =>
|
||||||
|
({ user }: AuthenticatedRequest, res, next) => {
|
||||||
|
if (authRole === 'none') return next();
|
||||||
|
|
||||||
|
if (!user) return res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||||
|
|
||||||
|
const { globalRole } = user;
|
||||||
|
if (authRole === 'any' || (globalRole.scope === authRole[0] && globalRole.name === authRole[1]))
|
||||||
|
return next();
|
||||||
|
|
||||||
|
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||||
|
};
|
||||||
|
|
||||||
export const registerController = (app: Application, config: Config, controller: object) => {
|
export const registerController = (app: Application, config: Config, controller: object) => {
|
||||||
const controllerClass = controller.constructor;
|
const controllerClass = controller.constructor;
|
||||||
|
@ -14,11 +40,16 @@ export const registerController = (app: Application, config: Config, controller:
|
||||||
if (!controllerBasePath)
|
if (!controllerBasePath)
|
||||||
throw new Error(`${controllerClass.name} is missing the RestController decorator`);
|
throw new Error(`${controllerClass.name} is missing the RestController decorator`);
|
||||||
|
|
||||||
|
const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as
|
||||||
|
| AuthRoleMetadata
|
||||||
|
| undefined;
|
||||||
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
|
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
|
||||||
if (routes.length > 0) {
|
if (routes.length > 0) {
|
||||||
const router = Router({ mergeParams: true });
|
const router = Router({ mergeParams: true });
|
||||||
const restBasePath = config.getEnv('endpoints.rest');
|
const restBasePath = config.getEnv('endpoints.rest');
|
||||||
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`.replace(/\/+/g, '/');
|
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`
|
||||||
|
.replace(/\/+/g, '/')
|
||||||
|
.replace(/\/$/, '');
|
||||||
|
|
||||||
const controllerMiddlewares = (
|
const controllerMiddlewares = (
|
||||||
(Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[]
|
(Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[]
|
||||||
|
@ -28,8 +59,10 @@ export const registerController = (app: Application, config: Config, controller:
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.forEach(({ method, path, middlewares: routeMiddlewares, handlerName }) => {
|
routes.forEach(({ method, path, middlewares: routeMiddlewares, handlerName }) => {
|
||||||
|
const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']);
|
||||||
router[method](
|
router[method](
|
||||||
path,
|
path,
|
||||||
|
...(authRole ? [createAuthMiddleware(authRole)] : []),
|
||||||
...controllerMiddlewares,
|
...controllerMiddlewares,
|
||||||
...routeMiddlewares,
|
...routeMiddlewares,
|
||||||
send(async (req: Request, res: Response) =>
|
send(async (req: Request, res: Response) =>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import type { Request, Response, RequestHandler } from 'express';
|
import type { Request, Response, RequestHandler } from 'express';
|
||||||
|
import type { RoleNames, RoleScopes } from '@db/entities/Role';
|
||||||
|
|
||||||
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||||
|
|
||||||
|
export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none';
|
||||||
|
export type AuthRoleMetadata = Record<string, AuthRole>;
|
||||||
|
|
||||||
export interface MiddlewareMetadata {
|
export interface MiddlewareMetadata {
|
||||||
handlerName: string;
|
handlerName: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,15 +28,13 @@ import type {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { MessageEventBusDestinationTypeNames, EventMessageTypeNames } from 'n8n-workflow';
|
import { MessageEventBusDestinationTypeNames, EventMessageTypeNames } from 'n8n-workflow';
|
||||||
import type { User } from '@db/entities/User';
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
|
||||||
import type { EventMessageNodeOptions } from './EventMessageClasses/EventMessageNode';
|
import type { EventMessageNodeOptions } from './EventMessageClasses/EventMessageNode';
|
||||||
import { EventMessageNode } from './EventMessageClasses/EventMessageNode';
|
import { EventMessageNode } from './EventMessageClasses/EventMessageNode';
|
||||||
import { recoverExecutionDataFromEventLogMessages } from './MessageEventBus/recoverEvents';
|
import { recoverExecutionDataFromEventLogMessages } from './MessageEventBus/recoverEvents';
|
||||||
import { RestController, Get, Post, Delete } from '@/decorators';
|
import { RestController, Get, Post, Delete, Authorized } from '@/decorators';
|
||||||
import type { MessageEventBusDestination } from './MessageEventBusDestination/MessageEventBusDestination.ee';
|
import type { MessageEventBusDestination } from './MessageEventBusDestination/MessageEventBusDestination.ee';
|
||||||
import { isOwnerMiddleware } from '../middlewares/isOwner';
|
|
||||||
import type { DeleteResult } from 'typeorm';
|
import type { DeleteResult } from 'typeorm';
|
||||||
|
import { AuthenticatedRequest } from '@/requests';
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// TypeGuards
|
// TypeGuards
|
||||||
|
@ -74,12 +72,14 @@ const isMessageEventBusDestinationOptions = (
|
||||||
// Controller
|
// Controller
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
|
@Authorized()
|
||||||
@RestController('/eventbus')
|
@RestController('/eventbus')
|
||||||
export class EventBusController {
|
export class EventBusController {
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Events
|
// Events
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
@Get('/event', { middlewares: [isOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
|
@Get('/event')
|
||||||
async getEvents(
|
async getEvents(
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
): Promise<EventMessageTypes[] | Record<string, EventMessageTypes[]>> {
|
): Promise<EventMessageTypes[] | Record<string, EventMessageTypes[]>> {
|
||||||
|
@ -132,7 +132,8 @@ export class EventBusController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/event', { middlewares: [isOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
|
@Post('/event')
|
||||||
async postEvent(req: express.Request): Promise<EventMessageTypes | undefined> {
|
async postEvent(req: express.Request): Promise<EventMessageTypes | undefined> {
|
||||||
let msg: EventMessageTypes | undefined;
|
let msg: EventMessageTypes | undefined;
|
||||||
if (isEventMessageOptions(req.body)) {
|
if (isEventMessageOptions(req.body)) {
|
||||||
|
@ -172,12 +173,9 @@ export class EventBusController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/destination', { middlewares: [isOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
async postDestination(req: express.Request): Promise<any> {
|
@Post('/destination')
|
||||||
if (!req.user || (req.user as User).globalRole.name !== 'owner') {
|
async postDestination(req: AuthenticatedRequest): Promise<any> {
|
||||||
throw new ResponseHelper.UnauthorizedError('Invalid request');
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: MessageEventBusDestination | undefined;
|
let result: MessageEventBusDestination | undefined;
|
||||||
if (isMessageEventBusDestinationOptions(req.body)) {
|
if (isMessageEventBusDestinationOptions(req.body)) {
|
||||||
switch (req.body.__type) {
|
switch (req.body.__type) {
|
||||||
|
@ -228,11 +226,9 @@ export class EventBusController {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('/destination', { middlewares: [isOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
async deleteDestination(req: express.Request): Promise<DeleteResult | undefined> {
|
@Delete('/destination')
|
||||||
if (!req.user || (req.user as User).globalRole.name !== 'owner') {
|
async deleteDestination(req: AuthenticatedRequest): Promise<DeleteResult | undefined> {
|
||||||
throw new ResponseHelper.UnauthorizedError('Invalid request');
|
|
||||||
}
|
|
||||||
if (isWithIdString(req.query)) {
|
if (isWithIdString(req.query)) {
|
||||||
return eventBus.removeDestination(req.query.id);
|
return eventBus.removeDestination(req.query.id);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -10,13 +10,7 @@ import type { AuthenticatedRequest } from '@/requests';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants';
|
import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants';
|
||||||
import { issueCookie, resolveJwtContent } from '@/auth/jwt';
|
import { issueCookie, resolveJwtContent } from '@/auth/jwt';
|
||||||
import {
|
import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
|
||||||
isAuthenticatedRequest,
|
|
||||||
isAuthExcluded,
|
|
||||||
isPostUsersId,
|
|
||||||
isUserManagementEnabled,
|
|
||||||
} from '@/UserManagement/UserManagementHelper';
|
|
||||||
import { SamlUrls } from '@/sso/saml/constants';
|
|
||||||
import type { UserRepository } from '@db/repositories';
|
import type { UserRepository } from '@db/repositories';
|
||||||
|
|
||||||
const jwtFromRequest = (req: Request) => {
|
const jwtFromRequest = (req: Request) => {
|
||||||
|
@ -66,6 +60,17 @@ const staticAssets = globSync(['**/*.html', '**/*.svg', '**/*.png', '**/*.ico'],
|
||||||
cwd: EDITOR_UI_DIST_DIR,
|
cwd: EDITOR_UI_DIST_DIR,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: delete this
|
||||||
|
const isPostUsersId = (req: Request, restEndpoint: string): boolean =>
|
||||||
|
req.method === 'POST' &&
|
||||||
|
new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url) &&
|
||||||
|
!req.url.includes('reinvite');
|
||||||
|
|
||||||
|
const isAuthExcluded = (url: string, ignoredEndpoints: Readonly<string[]>): boolean =>
|
||||||
|
!!ignoredEndpoints
|
||||||
|
.filter(Boolean) // skip empty paths
|
||||||
|
.find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This sets up the auth middlewares in the correct order
|
* This sets up the auth middlewares in the correct order
|
||||||
*/
|
*/
|
||||||
|
@ -85,20 +90,16 @@ export const setupAuthMiddlewares = (
|
||||||
// skip authentication for preflight requests
|
// skip authentication for preflight requests
|
||||||
req.method === 'OPTIONS' ||
|
req.method === 'OPTIONS' ||
|
||||||
staticAssets.includes(req.url.slice(1)) ||
|
staticAssets.includes(req.url.slice(1)) ||
|
||||||
|
isAuthExcluded(req.url, ignoredEndpoints) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/settings`) ||
|
req.url.startsWith(`/${restEndpoint}/settings`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/login`) ||
|
req.url.startsWith(`/${restEndpoint}/login`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/logout`) ||
|
|
||||||
req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) ||
|
req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) ||
|
||||||
isPostUsersId(req, restEndpoint) ||
|
isPostUsersId(req, restEndpoint) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/forgot-password`) ||
|
req.url.startsWith(`/${restEndpoint}/forgot-password`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/resolve-password-token`) ||
|
req.url.startsWith(`/${restEndpoint}/resolve-password-token`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
|
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ||
|
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`)
|
||||||
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.metadata}`) ||
|
|
||||||
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.initSSO}`) ||
|
|
||||||
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.acs}`) ||
|
|
||||||
isAuthExcluded(req.url, ignoredEndpoints)
|
|
||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -115,43 +116,5 @@ export const setupAuthMiddlewares = (
|
||||||
return passportMiddleware(req, res, next);
|
return passportMiddleware(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use((req: Request | AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
||||||
// req.user is empty for public routes, so just proceed
|
|
||||||
// owner can do anything, so proceed as well
|
|
||||||
if (!req.user || (isAuthenticatedRequest(req) && req.user.globalRole.name === 'owner')) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Not owner and user exists. We now protect restricted urls.
|
|
||||||
const postRestrictedUrls = [
|
|
||||||
`/${restEndpoint}/users`,
|
|
||||||
`/${restEndpoint}/owner`,
|
|
||||||
`/${restEndpoint}/ldap/sync`,
|
|
||||||
`/${restEndpoint}/ldap/test-connection`,
|
|
||||||
];
|
|
||||||
const getRestrictedUrls = [`/${restEndpoint}/ldap/sync`, `/${restEndpoint}/ldap/config`];
|
|
||||||
const putRestrictedUrls = [`/${restEndpoint}/ldap/config`];
|
|
||||||
const trimmedUrl = req.url.endsWith('/') ? req.url.slice(0, -1) : req.url;
|
|
||||||
if (
|
|
||||||
(req.method === 'POST' && postRestrictedUrls.includes(trimmedUrl)) ||
|
|
||||||
(req.method === 'GET' && getRestrictedUrls.includes(trimmedUrl)) ||
|
|
||||||
(req.method === 'PUT' && putRestrictedUrls.includes(trimmedUrl)) ||
|
|
||||||
(req.method === 'DELETE' &&
|
|
||||||
new RegExp(`/${restEndpoint}/users/[^/]+`, 'gm').test(trimmedUrl)) ||
|
|
||||||
(req.method === 'POST' &&
|
|
||||||
new RegExp(`/${restEndpoint}/users/[^/]+/reinvite`, 'gm').test(trimmedUrl)) ||
|
|
||||||
new RegExp(`/${restEndpoint}/owner/[^/]+`, 'gm').test(trimmedUrl)
|
|
||||||
) {
|
|
||||||
Logger.verbose('User attempted to access endpoint without authorization', {
|
|
||||||
endpoint: `${req.method} ${trimmedUrl}`,
|
|
||||||
userId: isAuthenticatedRequest(req) ? req.user.id : 'unknown',
|
|
||||||
});
|
|
||||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(refreshExpiringCookie);
|
app.use(refreshExpiringCookie);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import type { RequestHandler } from 'express';
|
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
|
||||||
import type { AuthenticatedRequest } from '@/requests';
|
|
||||||
|
|
||||||
export const isOwnerMiddleware: RequestHandler = (req: AuthenticatedRequest, res, next) => {
|
|
||||||
if (req.user.globalRole.name === 'owner') {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
LoggerProxy.debug('Request failed because user is not owner');
|
|
||||||
res.status(401).send('Unauthorized');
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,24 +1,11 @@
|
||||||
import type { RequestHandler } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
import type { AuthenticatedRequest } from '@/requests';
|
|
||||||
import { isSamlLicensed, isSamlLicensedAndEnabled } from '../samlHelpers';
|
import { isSamlLicensed, isSamlLicensedAndEnabled } from '../samlHelpers';
|
||||||
|
|
||||||
export const samlLicensedOwnerMiddleware: RequestHandler = (
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res,
|
|
||||||
next,
|
|
||||||
) => {
|
|
||||||
if (isSamlLicensed() && req.user?.globalRole.name === 'owner') {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
|
export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
|
||||||
if (isSamlLicensedAndEnabled()) {
|
if (isSamlLicensedAndEnabled()) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,6 +13,6 @@ export const samlLicensedMiddleware: RequestHandler = (req, res, next) => {
|
||||||
if (isSamlLicensed()) {
|
if (isSamlLicensed()) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { Get, Post, RestController } from '@/decorators';
|
import { Authorized, Get, Post, RestController } from '@/decorators';
|
||||||
import { SamlUrls } from '../constants';
|
import { SamlUrls } from '../constants';
|
||||||
import {
|
import {
|
||||||
samlLicensedAndEnabledMiddleware,
|
samlLicensedAndEnabledMiddleware,
|
||||||
samlLicensedMiddleware,
|
samlLicensedMiddleware,
|
||||||
samlLicensedOwnerMiddleware,
|
|
||||||
} from '../middleware/samlEnabledMiddleware';
|
} from '../middleware/samlEnabledMiddleware';
|
||||||
import { SamlService } from '../saml.service.ee';
|
import { SamlService } from '../saml.service.ee';
|
||||||
import { SamlConfiguration } from '../types/requests';
|
import { SamlConfiguration } from '../types/requests';
|
||||||
|
@ -39,7 +38,8 @@ export class SamlController {
|
||||||
* GET /sso/saml/config
|
* GET /sso/saml/config
|
||||||
* Return SAML config
|
* Return SAML config
|
||||||
*/
|
*/
|
||||||
@Get(SamlUrls.config, { middlewares: [samlLicensedOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
|
@Get(SamlUrls.config, { middlewares: [samlLicensedMiddleware] })
|
||||||
async configGet() {
|
async configGet() {
|
||||||
const prefs = this.samlService.samlPreferences;
|
const prefs = this.samlService.samlPreferences;
|
||||||
return {
|
return {
|
||||||
|
@ -53,7 +53,8 @@ export class SamlController {
|
||||||
* POST /sso/saml/config
|
* POST /sso/saml/config
|
||||||
* Set SAML config
|
* Set SAML config
|
||||||
*/
|
*/
|
||||||
@Post(SamlUrls.config, { middlewares: [samlLicensedOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
|
@Post(SamlUrls.config, { middlewares: [samlLicensedMiddleware] })
|
||||||
async configPost(req: SamlConfiguration.Update) {
|
async configPost(req: SamlConfiguration.Update) {
|
||||||
const validationResult = await validate(req.body);
|
const validationResult = await validate(req.body);
|
||||||
if (validationResult.length === 0) {
|
if (validationResult.length === 0) {
|
||||||
|
@ -71,7 +72,8 @@ export class SamlController {
|
||||||
* POST /sso/saml/config/toggle
|
* POST /sso/saml/config/toggle
|
||||||
* Set SAML config
|
* Set SAML config
|
||||||
*/
|
*/
|
||||||
@Post(SamlUrls.configToggleEnabled, { middlewares: [samlLicensedOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
|
@Post(SamlUrls.configToggleEnabled, { middlewares: [samlLicensedMiddleware] })
|
||||||
async toggleEnabledPost(req: SamlConfiguration.Toggle, res: express.Response) {
|
async toggleEnabledPost(req: SamlConfiguration.Toggle, res: express.Response) {
|
||||||
if (req.body.loginEnabled === undefined) {
|
if (req.body.loginEnabled === undefined) {
|
||||||
throw new BadRequestError('Body should contain a boolean "loginEnabled" property');
|
throw new BadRequestError('Body should contain a boolean "loginEnabled" property');
|
||||||
|
@ -155,7 +157,8 @@ export class SamlController {
|
||||||
* Test SAML config
|
* Test SAML config
|
||||||
* This endpoint is available if SAML is licensed and the requestor is an instance owner
|
* This endpoint is available if SAML is licensed and the requestor is an instance owner
|
||||||
*/
|
*/
|
||||||
@Get(SamlUrls.configTest, { middlewares: [samlLicensedOwnerMiddleware] })
|
@Authorized(['global', 'owner'])
|
||||||
|
@Get(SamlUrls.configTest, { middlewares: [samlLicensedMiddleware] })
|
||||||
async configTestGet(req: AuthenticatedRequest, res: express.Response) {
|
async configTestGet(req: AuthenticatedRequest, res: express.Response) {
|
||||||
return this.handleInitSSO(res, getServiceProviderConfigTestReturnUrl());
|
return this.handleInitSSO(res, getServiceProviderConfigTestReturnUrl());
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly<string[]> = [
|
||||||
'POST /users',
|
'POST /users',
|
||||||
'DELETE /users/123',
|
'DELETE /users/123',
|
||||||
'POST /users/123/reinvite',
|
'POST /users/123/reinvite',
|
||||||
'POST /owner/pre-setup',
|
'GET /owner/pre-setup',
|
||||||
'POST /owner/setup',
|
'POST /owner/setup',
|
||||||
'POST /owner/skip-setup',
|
'POST /owner/skip-setup',
|
||||||
];
|
];
|
||||||
|
|
|
@ -31,6 +31,7 @@ let credentialOwnerRole: Role;
|
||||||
let owner: User;
|
let owner: User;
|
||||||
let authlessAgent: SuperAgentTest;
|
let authlessAgent: SuperAgentTest;
|
||||||
let authOwnerAgent: SuperAgentTest;
|
let authOwnerAgent: SuperAgentTest;
|
||||||
|
let authAgentFor: (user: User) => SuperAgentTest;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const app = await utils.initTestServer({ endpointGroups: ['users'] });
|
const app = await utils.initTestServer({ endpointGroups: ['users'] });
|
||||||
|
@ -49,7 +50,8 @@ beforeAll(async () => {
|
||||||
owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
|
|
||||||
authlessAgent = utils.createAgent(app);
|
authlessAgent = utils.createAgent(app);
|
||||||
authOwnerAgent = utils.createAuthAgent(app)(owner);
|
authAgentFor = utils.createAuthAgent(app);
|
||||||
|
authOwnerAgent = authAgentFor(owner);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -69,7 +71,7 @@ afterAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /users', () => {
|
describe('GET /users', () => {
|
||||||
test('should return all users', async () => {
|
test('should return all users (for owner)', async () => {
|
||||||
await testDb.createUser({ globalRole: globalMemberRole });
|
await testDb.createUser({ globalRole: globalMemberRole });
|
||||||
|
|
||||||
const response = await authOwnerAgent.get('/users');
|
const response = await authOwnerAgent.get('/users');
|
||||||
|
@ -103,6 +105,14 @@ describe('GET /users', () => {
|
||||||
expect(apiKey).not.toBeDefined();
|
expect(apiKey).not.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return all users (for member)', async () => {
|
||||||
|
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||||
|
const response = await authAgentFor(member).get('/users');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data.length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /users/:id', () => {
|
describe('DELETE /users/:id', () => {
|
||||||
|
|
Loading…
Reference in a new issue