mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
refactor(core): Enforce authorization by default on all routes (no-changelog) (#8762)
This commit is contained in:
parent
2811f77798
commit
db4a419c8d
|
@ -1,6 +1,7 @@
|
|||
export type DefaultOperations = 'create' | 'read' | 'update' | 'delete' | 'list';
|
||||
export type Resource =
|
||||
| 'auditLogs'
|
||||
| 'banner'
|
||||
| 'communityPackage'
|
||||
| 'credential'
|
||||
| 'externalSecretsProvider'
|
||||
|
@ -27,6 +28,7 @@ export type ResourceScope<
|
|||
export type WildcardScope = `${Resource}:*` | '*';
|
||||
|
||||
export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>;
|
||||
export type BannerScope = ResourceScope<'banner', 'dismiss'>;
|
||||
export type CommunityPackageScope = ResourceScope<
|
||||
'communityPackage',
|
||||
'install' | 'uninstall' | 'update' | 'list' | 'manage'
|
||||
|
@ -56,6 +58,7 @@ export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share
|
|||
|
||||
export type Scope =
|
||||
| AuditLogsScope
|
||||
| BannerScope
|
||||
| CommunityPackageScope
|
||||
| CredentialScope
|
||||
| ExternalSecretProviderScope
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Authorized, Get, Post, RestController, GlobalScope } from '@/decorators';
|
||||
import { Get, Post, RestController, GlobalScope } from '@/decorators';
|
||||
import { ExternalSecretsRequest } from '@/requests';
|
||||
import { Response } from 'express';
|
||||
import { ExternalSecretsService } from './ExternalSecrets.service.ee';
|
||||
import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/external-secrets')
|
||||
export class ExternalSecretsController {
|
||||
constructor(private readonly secretsService: ExternalSecretsService) {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import pick from 'lodash/pick';
|
||||
import { Authorized, Get, Post, Put, RestController, GlobalScope } from '@/decorators';
|
||||
import { Get, Post, Put, RestController, GlobalScope } from '@/decorators';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
||||
|
@ -8,7 +8,6 @@ import { getLdapSynchronizations } from './helpers';
|
|||
import { LdapConfiguration } from './types';
|
||||
import { LdapService } from './ldap.service';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/ldap')
|
||||
export class LdapController {
|
||||
constructor(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Service } from 'typedi';
|
||||
import type { Response } from 'express';
|
||||
import type { NextFunction, Response } from 'express';
|
||||
import { createHash } from 'crypto';
|
||||
import { JsonWebTokenError, TokenExpiredError, type JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
|
@ -7,12 +7,11 @@ import config from '@/config';
|
|||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import type { AuthRole } from '@/decorators/types';
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||
import { License } from '@/License';
|
||||
import { Logger } from '@/Logger';
|
||||
import type { AuthenticatedRequest, AsyncRequestHandler } from '@/requests';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
|
@ -31,54 +30,33 @@ interface IssuedJWT extends AuthJwtPayload {
|
|||
|
||||
@Service()
|
||||
export class AuthService {
|
||||
private middlewareCache = new Map<string, AsyncRequestHandler>();
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly license: License,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly urlService: UrlService,
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.authMiddleware = this.authMiddleware.bind(this);
|
||||
}
|
||||
|
||||
createAuthMiddleware(authRole: AuthRole): AsyncRequestHandler {
|
||||
const { middlewareCache: cache } = this;
|
||||
let authMiddleware = cache.get(authRole);
|
||||
if (authMiddleware) return authMiddleware;
|
||||
|
||||
authMiddleware = async (req: AuthenticatedRequest, res, next) => {
|
||||
if (authRole === 'none') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const token = req.cookies[AUTH_COOKIE_NAME];
|
||||
if (token) {
|
||||
try {
|
||||
req.user = await this.resolveJwt(token, res);
|
||||
} catch (error) {
|
||||
if (error instanceof JsonWebTokenError || error instanceof AuthError) {
|
||||
this.clearCookie(res);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
async authMiddleware(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
const token = req.cookies[AUTH_COOKIE_NAME];
|
||||
if (token) {
|
||||
try {
|
||||
req.user = await this.resolveJwt(token, res);
|
||||
} catch (error) {
|
||||
if (error instanceof JsonWebTokenError || error instanceof AuthError) {
|
||||
this.clearCookie(res);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (authRole === 'any' || authRole === req.user.role) {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ status: 'error', message: 'Forbidden' });
|
||||
}
|
||||
};
|
||||
|
||||
cache.set(authRole, authMiddleware);
|
||||
return authMiddleware;
|
||||
if (req.user) next();
|
||||
else res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
clearCookie(res: Response) {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Authorized, Get, RestController } from '@/decorators';
|
||||
import { Get, RestController } from '@/decorators';
|
||||
import { ActiveWorkflowRequest } from '@/requests';
|
||||
import { ActiveWorkflowsService } from '@/services/activeWorkflows.service';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/active-workflows')
|
||||
export class ActiveWorkflowsController {
|
||||
constructor(private readonly activeWorkflowsService: ActiveWorkflowsService) {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import validator from 'validator';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { Authorized, Get, Post, RestController } from '@/decorators';
|
||||
import { Get, Post, RestController } from '@/decorators';
|
||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { Request, Response } from 'express';
|
||||
import type { User } from '@db/entities/User';
|
||||
|
@ -38,10 +38,8 @@ export class AuthController {
|
|||
private readonly postHog?: PostHogClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Log in a user.
|
||||
*/
|
||||
@Post('/login')
|
||||
/** Log in a user */
|
||||
@Post('/login', { skipAuth: true })
|
||||
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
||||
const { email, password, mfaToken, mfaRecoveryCode } = req.body;
|
||||
if (!email) throw new ApplicationError('Email is required to log in');
|
||||
|
@ -113,7 +111,6 @@ export class AuthController {
|
|||
}
|
||||
|
||||
/** Check if the user is already logged in */
|
||||
@Authorized()
|
||||
@Get('/login')
|
||||
async currentUser(req: AuthenticatedRequest): Promise<PublicUser> {
|
||||
return await this.userService.toPublic(req.user, {
|
||||
|
@ -122,10 +119,8 @@ export class AuthController {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate invite token to enable invitee to set up their account.
|
||||
*/
|
||||
@Get('/resolve-signup-token')
|
||||
/** Validate invite token to enable invitee to set up their account */
|
||||
@Get('/resolve-signup-token', { skipAuth: true })
|
||||
async resolveSignupToken(req: UserRequest.ResolveSignUp) {
|
||||
const { inviterId, inviteeId } = req.query;
|
||||
const isWithinUsersLimit = this.license.isWithinUsersLimit();
|
||||
|
@ -192,10 +187,7 @@ export class AuthController {
|
|||
return { inviter: { firstName, lastName } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out a user.
|
||||
*/
|
||||
@Authorized()
|
||||
/** Log out a user */
|
||||
@Post('/logout')
|
||||
logout(_: Request, res: Response) {
|
||||
this.authService.clearCookie(res);
|
||||
|
|
|
@ -5,16 +5,7 @@ import {
|
|||
STARTER_TEMPLATE_NAME,
|
||||
UNKNOWN_FAILURE_REASON,
|
||||
} from '@/constants';
|
||||
import {
|
||||
Authorized,
|
||||
Delete,
|
||||
Get,
|
||||
Middleware,
|
||||
Patch,
|
||||
Post,
|
||||
RestController,
|
||||
GlobalScope,
|
||||
} from '@/decorators';
|
||||
import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } from '@/decorators';
|
||||
import { NodeRequest } from '@/requests';
|
||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import type { CommunityPackages } from '@/Interfaces';
|
||||
|
@ -41,7 +32,6 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
|
|||
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@RestController('/community-packages')
|
||||
export class CommunityPackagesController {
|
||||
constructor(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import express from 'express';
|
||||
import { Authorized, Get, RestController } from '@/decorators';
|
||||
import { Get, RestController } from '@/decorators';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import { CtaService } from '@/services/cta.service';
|
||||
|
||||
|
@ -7,7 +7,6 @@ import { CtaService } from '@/services/cta.service';
|
|||
* Controller for Call to Action (CTA) endpoints. CTAs are certain
|
||||
* messages that are shown to users in the UI.
|
||||
*/
|
||||
@Authorized()
|
||||
@RestController('/cta')
|
||||
export class CtaController {
|
||||
constructor(private readonly ctaService: CtaService) {}
|
||||
|
|
|
@ -11,7 +11,7 @@ export class DebugController {
|
|||
private readonly workflowRepository: WorkflowRepository,
|
||||
) {}
|
||||
|
||||
@Get('/multi-main-setup')
|
||||
@Get('/multi-main-setup', { skipAuth: true })
|
||||
async getMultiMainSetupDetails() {
|
||||
const leaderKey = await this.orchestrationService.multiMainSetup.fetchLeaderKey();
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
|
||||
import { Authorized, Get, Middleware, RestController } from '@/decorators';
|
||||
import { Get, Middleware, RestController } from '@/decorators';
|
||||
import { getBase } from '@/WorkflowExecuteAdditionalData';
|
||||
import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service';
|
||||
import { DynamicNodeParametersRequest } from '@/requests';
|
||||
|
@ -21,17 +21,12 @@ const assertMethodName: RequestHandler = (req, res, next) => {
|
|||
next();
|
||||
};
|
||||
|
||||
@Authorized()
|
||||
@RestController('/dynamic-node-parameters')
|
||||
export class DynamicNodeParametersController {
|
||||
constructor(private readonly service: DynamicNodeParametersService) {}
|
||||
|
||||
@Middleware()
|
||||
parseQueryParams(
|
||||
req: DynamicNodeParametersRequest.BaseRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
parseQueryParams(req: DynamicNodeParametersRequest.BaseRequest, _: Response, next: NextFunction) {
|
||||
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.query;
|
||||
if (!nodeTypeAndVersion) {
|
||||
throw new BadRequestError('Parameter nodeTypeAndVersion is required.');
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
|||
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
||||
import { License } from '@/License';
|
||||
import { LICENSE_FEATURES, inE2ETests } from '@/constants';
|
||||
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
|
||||
import { Patch, Post, RestController } from '@/decorators';
|
||||
import type { UserSetupPayload } from '@/requests';
|
||||
import type { BooleanLicenseFeature, IPushDataType } from '@/Interfaces';
|
||||
import { MfaService } from '@/Mfa/mfa.service';
|
||||
|
@ -60,7 +60,6 @@ type PushRequest = Request<
|
|||
}
|
||||
>;
|
||||
|
||||
@NoAuthRequired()
|
||||
@RestController('/e2e')
|
||||
export class E2EController {
|
||||
private enabledFeatures: Record<BooleanLicenseFeature, boolean> = {
|
||||
|
@ -97,7 +96,7 @@ export class E2EController {
|
|||
this.enabledFeatures[feature] ?? false;
|
||||
}
|
||||
|
||||
@Post('/reset')
|
||||
@Post('/reset', { skipAuth: true })
|
||||
async reset(req: ResetRequest) {
|
||||
this.resetFeatures();
|
||||
await this.resetLogStreaming();
|
||||
|
@ -107,18 +106,18 @@ export class E2EController {
|
|||
await this.setupUserManagement(req.body.owner, req.body.members, req.body.admin);
|
||||
}
|
||||
|
||||
@Post('/push')
|
||||
@Post('/push', { skipAuth: true })
|
||||
async pushSend(req: PushRequest) {
|
||||
this.push.broadcast(req.body.type, req.body.data);
|
||||
}
|
||||
|
||||
@Patch('/feature')
|
||||
@Patch('/feature', { skipAuth: true })
|
||||
setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) {
|
||||
const { enabled, feature } = req.body;
|
||||
this.enabledFeatures[feature] = enabled;
|
||||
}
|
||||
|
||||
@Patch('/queue-mode')
|
||||
@Patch('/queue-mode', { skipAuth: true })
|
||||
async setQueueMode(req: Request<{}, {}, { enabled: boolean }>) {
|
||||
const { enabled } = req.body;
|
||||
config.set('executions.mode', enabled ? 'queue' : 'regular');
|
||||
|
|
|
@ -3,7 +3,7 @@ import validator from 'validator';
|
|||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
import { Authorized, NoAuthRequired, Post, GlobalScope, RestController } from '@/decorators';
|
||||
import { Post, GlobalScope, RestController } from '@/decorators';
|
||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { UserRequest } from '@/requests';
|
||||
import { License } from '@/License';
|
||||
|
@ -19,7 +19,6 @@ import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
|||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/invitations')
|
||||
export class InvitationController {
|
||||
constructor(
|
||||
|
@ -120,8 +119,7 @@ export class InvitationController {
|
|||
/**
|
||||
* Fill out user shell with first name, last name, and password.
|
||||
*/
|
||||
@NoAuthRequired()
|
||||
@Post('/:id/accept')
|
||||
@Post('/:id/accept', { skipAuth: true })
|
||||
async acceptInvitation(req: UserRequest.Update, res: Response) {
|
||||
const { id: inviteeId } = req.params;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Response } from 'express';
|
|||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
|
||||
import { Delete, Get, Patch, Post, RestController } from '@/decorators';
|
||||
import { PasswordUtility } from '@/services/password.utility';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import type { User } from '@db/entities/User';
|
||||
|
@ -23,7 +23,6 @@ import { InternalHooks } from '@/InternalHooks';
|
|||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/me')
|
||||
export class MeController {
|
||||
constructor(
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Authorized, Delete, Get, Post, RestController } from '@/decorators';
|
||||
import { Delete, Get, Post, RestController } from '@/decorators';
|
||||
import { AuthenticatedRequest, MFA } from '@/requests';
|
||||
import { MfaService } from '@/Mfa/mfa.service';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/mfa')
|
||||
export class MFAController {
|
||||
constructor(private mfaService: MfaService) {}
|
||||
|
|
|
@ -2,11 +2,10 @@ import { readFile } from 'fs/promises';
|
|||
import get from 'lodash/get';
|
||||
import { Request } from 'express';
|
||||
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
||||
import { Authorized, Post, RestController } from '@/decorators';
|
||||
import { Post, RestController } from '@/decorators';
|
||||
import config from '@/config';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/node-types')
|
||||
export class NodeTypesController {
|
||||
constructor(private readonly nodeTypes: NodeTypes) {}
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { RequestOptions } from 'oauth-1.0a';
|
|||
import clientOAuth1 from 'oauth-1.0a';
|
||||
import { createHmac } from 'crypto';
|
||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { Authorized, Get, RestController } from '@/decorators';
|
||||
import { Get, RestController } from '@/decorators';
|
||||
import { OAuthRequest } from '@/requests';
|
||||
import { sendErrorResponse } from '@/ResponseHelper';
|
||||
import { AbstractOAuthController } from './abstractOAuth.controller';
|
||||
|
@ -29,7 +29,6 @@ const algorithmMap = {
|
|||
/* eslint-enable */
|
||||
} as const;
|
||||
|
||||
@Authorized()
|
||||
@RestController('/oauth1-credential')
|
||||
export class OAuth1CredentialController extends AbstractOAuthController {
|
||||
override oauthVersion = 1;
|
||||
|
|
|
@ -8,7 +8,7 @@ import omit from 'lodash/omit';
|
|||
import set from 'lodash/set';
|
||||
import split from 'lodash/split';
|
||||
import { ApplicationError, jsonParse, jsonStringify } from 'n8n-workflow';
|
||||
import { Authorized, Get, RestController } from '@/decorators';
|
||||
import { Get, RestController } from '@/decorators';
|
||||
import { OAuthRequest } from '@/requests';
|
||||
import { AbstractOAuthController } from './abstractOAuth.controller';
|
||||
|
||||
|
@ -17,7 +17,6 @@ interface CsrfStateParam {
|
|||
token: string;
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@RestController('/oauth2-credential')
|
||||
export class OAuth2CredentialController extends AbstractOAuthController {
|
||||
override oauthVersion = 2;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Authorized, Post, RestController, GlobalScope } from '@/decorators';
|
||||
import { Post, RestController, GlobalScope } from '@/decorators';
|
||||
import { OrchestrationRequest } from '@/requests';
|
||||
import { OrchestrationService } from '@/services/orchestration.service';
|
||||
import { License } from '@/License';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/orchestration')
|
||||
export class OrchestrationController {
|
||||
constructor(
|
||||
|
@ -12,7 +11,7 @@ export class OrchestrationController {
|
|||
) {}
|
||||
|
||||
/**
|
||||
* These endpoints do not return anything, they just trigger the messsage to
|
||||
* These endpoints do not return anything, they just trigger the message to
|
||||
* the workers to respond on Redis with their status.
|
||||
*/
|
||||
@GlobalScope('orchestration:read')
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Response } from 'express';
|
|||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { Authorized, Post, RestController } from '@/decorators';
|
||||
import { GlobalScope, Post, RestController } from '@/decorators';
|
||||
import { PasswordUtility } from '@/services/password.utility';
|
||||
import { OwnerRequest } from '@/requests';
|
||||
import { SettingsRepository } from '@db/repositories/settings.repository';
|
||||
|
@ -15,7 +15,6 @@ import { Logger } from '@/Logger';
|
|||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
|
||||
@Authorized('global:owner')
|
||||
@RestController('/owner')
|
||||
export class OwnerController {
|
||||
constructor(
|
||||
|
@ -33,24 +32,19 @@ export class OwnerController {
|
|||
* Promote a shell into the owner of the n8n instance,
|
||||
* and enable `isInstanceOwnerSetUp` setting.
|
||||
*/
|
||||
@Post('/setup')
|
||||
@Post('/setup', { skipAuth: true })
|
||||
async setupOwner(req: OwnerRequest.Post, res: Response) {
|
||||
const { email, firstName, lastName, password } = req.body;
|
||||
const { id: userId } = req.user;
|
||||
|
||||
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
||||
this.logger.debug(
|
||||
'Request to claim instance ownership failed because instance owner already exists',
|
||||
{
|
||||
userId,
|
||||
},
|
||||
);
|
||||
throw new BadRequestError('Instance owner already setup');
|
||||
}
|
||||
|
||||
if (!email || !validator.isEmail(email)) {
|
||||
this.logger.debug('Request to claim instance ownership failed because of invalid email', {
|
||||
userId,
|
||||
invalidEmail: email,
|
||||
});
|
||||
throw new BadRequestError('Invalid email address');
|
||||
|
@ -61,25 +55,24 @@ export class OwnerController {
|
|||
if (!firstName || !lastName) {
|
||||
this.logger.debug(
|
||||
'Request to claim instance ownership failed because of missing first name or last name in payload',
|
||||
{ userId, payload: req.body },
|
||||
{ payload: req.body },
|
||||
);
|
||||
throw new BadRequestError('First and last names are mandatory');
|
||||
}
|
||||
|
||||
let owner = req.user;
|
||||
|
||||
Object.assign(owner, {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password: await this.passwordUtility.hash(validPassword),
|
||||
let owner = await this.userRepository.findOneOrFail({
|
||||
where: { role: 'global:owner' },
|
||||
});
|
||||
owner.email = email;
|
||||
owner.firstName = firstName;
|
||||
owner.lastName = lastName;
|
||||
owner.password = await this.passwordUtility.hash(validPassword);
|
||||
|
||||
await validateEntity(owner);
|
||||
|
||||
owner = await this.userRepository.save(owner, { transaction: false });
|
||||
|
||||
this.logger.info('Owner was set up successfully', { userId });
|
||||
this.logger.info('Owner was set up successfully');
|
||||
|
||||
await this.settingsRepository.update(
|
||||
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
||||
|
@ -88,19 +81,19 @@ export class OwnerController {
|
|||
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
|
||||
this.logger.debug('Setting isInstanceOwnerSetUp updated successfully', { userId });
|
||||
this.logger.debug('Setting isInstanceOwnerSetUp updated successfully');
|
||||
|
||||
this.authService.issueCookie(res, owner);
|
||||
|
||||
void this.internalHooks.onInstanceOwnerSetup({ user_id: userId });
|
||||
void this.internalHooks.onInstanceOwnerSetup({ user_id: owner.id });
|
||||
|
||||
return await this.userService.toPublic(owner, { posthog: this.postHog, withScopes: true });
|
||||
}
|
||||
|
||||
@Post('/dismiss-banner')
|
||||
@GlobalScope('banner:dismiss')
|
||||
async dismissBanner(req: OwnerRequest.DismissBanner) {
|
||||
const bannerName = 'banner' in req.body ? (req.body.banner as string) : '';
|
||||
const response = await this.settingsRepository.dismissBanner({ bannerName });
|
||||
return response;
|
||||
return await this.settingsRepository.dismissBanner({ bannerName });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export class PasswordResetController {
|
|||
*/
|
||||
@Post('/forgot-password', {
|
||||
middlewares: !inTest ? [throttle] : [],
|
||||
skipAuth: true,
|
||||
})
|
||||
async forgotPassword(req: PasswordResetRequest.Email) {
|
||||
if (!this.mailer.isEmailSetUp) {
|
||||
|
@ -150,7 +151,7 @@ export class PasswordResetController {
|
|||
/**
|
||||
* Verify password reset token and user ID.
|
||||
*/
|
||||
@Get('/resolve-password-token')
|
||||
@Get('/resolve-password-token', { skipAuth: true })
|
||||
async resolvePasswordToken(req: PasswordResetRequest.Credentials) {
|
||||
const { token } = req.query;
|
||||
|
||||
|
@ -182,7 +183,7 @@ export class PasswordResetController {
|
|||
/**
|
||||
* Verify password reset token and update password.
|
||||
*/
|
||||
@Post('/change-password')
|
||||
@Post('/change-password', { skipAuth: true })
|
||||
async changePassword(req: PasswordResetRequest.NewPassword, res: Response) {
|
||||
const { token, password, mfaToken } = req.body;
|
||||
|
||||
|
|
|
@ -1,20 +1,10 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import config from '@/config';
|
||||
import {
|
||||
Authorized,
|
||||
Delete,
|
||||
Get,
|
||||
Middleware,
|
||||
Patch,
|
||||
Post,
|
||||
RestController,
|
||||
GlobalScope,
|
||||
} from '@/decorators';
|
||||
import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } from '@/decorators';
|
||||
import { TagService } from '@/services/tag.service';
|
||||
import { TagsRequest } from '@/requests';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/tags')
|
||||
export class TagsController {
|
||||
private config = config;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Request } from 'express';
|
||||
import { join } from 'path';
|
||||
import { access } from 'fs/promises';
|
||||
import { Authorized, Get, RestController } from '@/decorators';
|
||||
import { Get, RestController } from '@/decorators';
|
||||
import config from '@/config';
|
||||
import { NODES_BASE_DIR } from '@/constants';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
@ -15,7 +15,6 @@ export declare namespace TranslationRequest {
|
|||
export type Credential = Request<{}, {}, {}, { credentialType: string }>;
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@RestController('/')
|
||||
export class TranslationController {
|
||||
constructor(private readonly credentialTypes: CredentialTypes) {}
|
||||
|
|
|
@ -4,15 +4,7 @@ import { AuthService } from '@/auth/auth.service';
|
|||
import { User } from '@db/entities/User';
|
||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import {
|
||||
GlobalScope,
|
||||
Authorized,
|
||||
Delete,
|
||||
Get,
|
||||
RestController,
|
||||
Patch,
|
||||
Licensed,
|
||||
} from '@/decorators';
|
||||
import { GlobalScope, Delete, Get, RestController, Patch, Licensed } from '@/decorators';
|
||||
import {
|
||||
ListQuery,
|
||||
UserRequest,
|
||||
|
@ -35,7 +27,6 @@ import { ExternalHooks } from '@/ExternalHooks';
|
|||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/users')
|
||||
export class UsersController {
|
||||
constructor(
|
||||
|
|
|
@ -11,14 +11,13 @@ import { License } from '@/License';
|
|||
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
|
||||
import { OwnershipService } from '@/services/ownership.service';
|
||||
import { EnterpriseCredentialsService } from './credentials.service.ee';
|
||||
import { Authorized, Delete, Get, Licensed, Patch, Post, Put, RestController } from '@/decorators';
|
||||
import { Delete, Get, Licensed, Patch, Post, Put, RestController } from '@/decorators';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import * as Db from '@/Db';
|
||||
import * as utils from '@/utils';
|
||||
import { listQueryMiddleware } from '@/middlewares';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/credentials')
|
||||
export class CredentialsController {
|
||||
constructor(
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/* 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');
|
|
@ -5,6 +5,8 @@ import type { Method, RouteMetadata } from './types';
|
|||
interface RouteOptions {
|
||||
middlewares?: RequestHandler[];
|
||||
usesTemplates?: boolean;
|
||||
/** When this flag is set to true, auth cookie isn't validated, and req.user will not be set */
|
||||
skipAuth?: boolean;
|
||||
}
|
||||
|
||||
const RouteFactory =
|
||||
|
@ -20,6 +22,7 @@ const RouteFactory =
|
|||
middlewares: options.middlewares ?? [],
|
||||
handlerName: String(handlerName),
|
||||
usesTemplates: options.usesTemplates ?? false,
|
||||
skipAuth: options.skipAuth ?? false,
|
||||
});
|
||||
Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
||||
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
||||
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
||||
export const CONTROLLER_AUTH_ROLES = 'CONTROLLER_AUTH_ROLES';
|
||||
export const CONTROLLER_LICENSE_FEATURES = 'CONTROLLER_LICENSE_FEATURES';
|
||||
export const CONTROLLER_REQUIRED_SCOPES = 'CONTROLLER_REQUIRED_SCOPES';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export { Authorized, NoAuthRequired } from './Authorized';
|
||||
export { RestController } from './RestController';
|
||||
export { Get, Post, Put, Patch, Delete } from './Route';
|
||||
export { Middleware } from './Middleware';
|
||||
|
|
|
@ -12,7 +12,6 @@ import { License } from '@/License';
|
|||
import type { AuthenticatedRequest } from '@/requests';
|
||||
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
||||
import {
|
||||
CONTROLLER_AUTH_ROLES,
|
||||
CONTROLLER_BASE_PATH,
|
||||
CONTROLLER_LICENSE_FEATURES,
|
||||
CONTROLLER_MIDDLEWARES,
|
||||
|
@ -20,7 +19,6 @@ import {
|
|||
CONTROLLER_ROUTES,
|
||||
} from './constants';
|
||||
import type {
|
||||
AuthRoleMetadata,
|
||||
Controller,
|
||||
LicenseMetadata,
|
||||
MiddlewareMetadata,
|
||||
|
@ -74,9 +72,6 @@ export const registerController = (app: Application, controllerClass: Class<obje
|
|||
extra: { controllerName: controllerClass.name },
|
||||
});
|
||||
|
||||
const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as
|
||||
| AuthRoleMetadata
|
||||
| undefined;
|
||||
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
|
||||
const licenseFeatures = Reflect.getMetadata(CONTROLLER_LICENSE_FEATURES, controllerClass) as
|
||||
| LicenseMetadata
|
||||
|
@ -99,15 +94,15 @@ export const registerController = (app: Application, controllerClass: Class<obje
|
|||
const authService = Container.get(AuthService);
|
||||
|
||||
routes.forEach(
|
||||
({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates }) => {
|
||||
const authRole = authRoles?.[handlerName] ?? authRoles?.['*'];
|
||||
({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates, skipAuth }) => {
|
||||
const features = licenseFeatures?.[handlerName] ?? licenseFeatures?.['*'];
|
||||
const scopes = requiredScopes?.[handlerName] ?? requiredScopes?.['*'];
|
||||
const handler = async (req: Request, res: Response) =>
|
||||
await controller[handlerName](req, res);
|
||||
router[method](
|
||||
path,
|
||||
...(authRole ? [authService.createAuthMiddleware(authRole)] : []),
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
...(skipAuth ? [] : [authService.authMiddleware]),
|
||||
...(features ? [createLicenseMiddleware(features)] : []),
|
||||
...(scopes ? [createGlobalScopeMiddleware(scopes)] : []),
|
||||
...controllerMiddlewares,
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import type { Request, Response, RequestHandler } from 'express';
|
||||
import type { GlobalRole } from '@db/entities/User';
|
||||
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
|
||||
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||
|
||||
export type AuthRole = GlobalRole | 'any' | 'none';
|
||||
export type AuthRoleMetadata = Record<string, AuthRole>;
|
||||
|
||||
export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>;
|
||||
|
||||
export type ScopeMetadata = Record<string, Scope[]>;
|
||||
|
@ -22,6 +18,7 @@ export interface RouteMetadata {
|
|||
handlerName: string;
|
||||
middlewares: RequestHandler[];
|
||||
usesTemplates: boolean;
|
||||
skipAuth: boolean;
|
||||
}
|
||||
|
||||
export type Controller = Record<
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { PullResult } from 'simple-git';
|
||||
import express from 'express';
|
||||
import { Authorized, Get, Post, Patch, RestController, GlobalScope } from '@/decorators';
|
||||
import { Get, Post, Patch, RestController, GlobalScope } from '@/decorators';
|
||||
import {
|
||||
sourceControlLicensedMiddleware,
|
||||
sourceControlLicensedAndEnabledMiddleware,
|
||||
|
@ -17,7 +17,6 @@ import { getRepoType } from './sourceControlHelper.ee';
|
|||
import { SourceControlGetStatus } from './types/sourceControlGetStatus';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/source-control')
|
||||
export class SourceControlController {
|
||||
constructor(
|
||||
|
@ -26,8 +25,7 @@ export class SourceControlController {
|
|||
private readonly internalHooks: InternalHooks,
|
||||
) {}
|
||||
|
||||
@Authorized('none')
|
||||
@Get('/preferences', { middlewares: [sourceControlLicensedMiddleware] })
|
||||
@Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true })
|
||||
async getPreferences(): Promise<SourceControlPreferences> {
|
||||
// returns the settings with the privateKey property redacted
|
||||
return this.sourceControlPreferencesService.getPreferences();
|
||||
|
@ -151,7 +149,6 @@ export class SourceControlController {
|
|||
}
|
||||
}
|
||||
|
||||
@Authorized('any')
|
||||
@Get('/get-branches', { middlewares: [sourceControlLicensedMiddleware] })
|
||||
async getBranches() {
|
||||
try {
|
||||
|
@ -212,7 +209,6 @@ export class SourceControlController {
|
|||
}
|
||||
}
|
||||
|
||||
@Authorized('any')
|
||||
@Get('/get-status', { middlewares: [sourceControlLicensedAndEnabledMiddleware] })
|
||||
async getStatus(req: SourceControlRequest.GetStatus) {
|
||||
try {
|
||||
|
@ -225,7 +221,6 @@ export class SourceControlController {
|
|||
}
|
||||
}
|
||||
|
||||
@Authorized('any')
|
||||
@Get('/status', { middlewares: [sourceControlLicensedMiddleware] })
|
||||
async status(req: SourceControlRequest.GetStatus) {
|
||||
try {
|
||||
|
|
|
@ -1,21 +1,11 @@
|
|||
import { VariablesRequest } from '@/requests';
|
||||
import {
|
||||
Authorized,
|
||||
Delete,
|
||||
Get,
|
||||
Licensed,
|
||||
Patch,
|
||||
Post,
|
||||
GlobalScope,
|
||||
RestController,
|
||||
} from '@/decorators';
|
||||
import { Delete, Get, Licensed, Patch, Post, GlobalScope, RestController } from '@/decorators';
|
||||
import { VariablesService } from './variables.service.ee';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { VariableValidationError } from '@/errors/variable-validation.error';
|
||||
import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/variables')
|
||||
export class VariablesController {
|
||||
constructor(private readonly variablesService: VariablesService) {}
|
||||
|
|
|
@ -5,7 +5,7 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
|
||||
|
||||
import { RestController, Get, Post, Delete, Authorized, GlobalScope } from '@/decorators';
|
||||
import { RestController, Get, Post, Delete, GlobalScope } from '@/decorators';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
||||
|
@ -52,7 +52,6 @@ const isMessageEventBusDestinationOptions = (
|
|||
// Controller
|
||||
// ----------------------------------------
|
||||
|
||||
@Authorized()
|
||||
@RestController('/eventbus')
|
||||
export class EventBusControllerEE {
|
||||
constructor(private readonly eventBus: MessageEventBus) {}
|
||||
|
|
|
@ -2,7 +2,7 @@ import express from 'express';
|
|||
import type { IRunExecutionData } from 'n8n-workflow';
|
||||
import { EventMessageTypeNames } from 'n8n-workflow';
|
||||
|
||||
import { RestController, Get, Post, Authorized, GlobalScope } from '@/decorators';
|
||||
import { RestController, Get, Post, GlobalScope } from '@/decorators';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
|
||||
import { isEventMessageOptions } from './EventMessageClasses/AbstractEventMessage';
|
||||
|
@ -33,7 +33,6 @@ const isWithQueryString = (candidate: unknown): candidate is { query: string } =
|
|||
// Controller
|
||||
// ----------------------------------------
|
||||
|
||||
@Authorized()
|
||||
@RestController('/eventbus')
|
||||
export class EventBusController {
|
||||
constructor(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { GetManyActiveFilter } from './execution.types';
|
||||
import { ExecutionRequest } from './execution.types';
|
||||
import { ExecutionService } from './execution.service';
|
||||
import { Authorized, Get, Post, RestController } from '@/decorators';
|
||||
import { Get, Post, RestController } from '@/decorators';
|
||||
import { EnterpriseExecutionsService } from './execution.service.ee';
|
||||
import { License } from '@/License';
|
||||
import { WorkflowSharingService } from '@/workflows/workflowSharing.service';
|
||||
|
@ -11,7 +11,6 @@ import { jsonParse } from 'n8n-workflow';
|
|||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { ActiveExecutionService } from './active-execution.service';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/executions')
|
||||
export class ExecutionsController {
|
||||
private readonly isQueueMode = config.getEnv('executions.mode') === 'queue';
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Authorized, Get, Post, GlobalScope, RestController } from '@/decorators';
|
||||
import { Get, Post, GlobalScope, RestController } from '@/decorators';
|
||||
import { LicenseRequest } from '@/requests';
|
||||
import { LicenseService } from './license.service';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/license')
|
||||
export class LicenseController {
|
||||
constructor(private readonly licenseService: LicenseService) {}
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { Scope } from '@n8n/permissions';
|
|||
|
||||
export const ownerPermissions: Scope[] = [
|
||||
'auditLogs:manage',
|
||||
'banner:dismiss',
|
||||
'credential:create',
|
||||
'credential:read',
|
||||
'credential:update',
|
||||
|
|
|
@ -104,7 +104,7 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => {
|
|||
|
||||
const pushValidationMiddleware: RequestHandler = async (
|
||||
req: SSEPushRequest | WebSocketPushRequest,
|
||||
res,
|
||||
_,
|
||||
next,
|
||||
) => {
|
||||
const ws = req.ws;
|
||||
|
@ -127,7 +127,8 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => {
|
|||
const authService = Container.get(AuthService);
|
||||
app.use(
|
||||
endpoint,
|
||||
authService.createAuthMiddleware('any'),
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
authService.authMiddleware,
|
||||
pushValidationMiddleware,
|
||||
(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => push.handleRequest(req, res),
|
||||
);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { ParsedQs } from 'qs';
|
||||
import type express from 'express';
|
||||
import type {
|
||||
BannerName,
|
||||
|
@ -21,17 +20,6 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
|||
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||
import type { WorkflowHistory } from '@db/entities/WorkflowHistory';
|
||||
|
||||
export type AsyncRequestHandler<
|
||||
Params = Record<string, string>,
|
||||
ResBody = unknown,
|
||||
ReqBody = unknown,
|
||||
ReqQuery = ParsedQs,
|
||||
> = (
|
||||
req: express.Request<Params, ResBody, ReqBody, ReqQuery>,
|
||||
res: express.Response<ResBody>,
|
||||
next: () => void,
|
||||
) => Promise<void>;
|
||||
|
||||
export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
|
|
@ -3,8 +3,7 @@ import { validate } from 'class-validator';
|
|||
import type { PostBindingContext } from 'samlify/types/src/entity';
|
||||
import url from 'url';
|
||||
|
||||
import { Authorized, Get, NoAuthRequired, Post, RestController, GlobalScope } from '@/decorators';
|
||||
|
||||
import { Get, Post, RestController, GlobalScope } from '@/decorators';
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
|
@ -31,7 +30,6 @@ import { SamlService } from '../saml.service.ee';
|
|||
import { SamlConfiguration } from '../types/requests';
|
||||
import { getInitSSOFormView } from '../views/initSsoPost';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/sso/saml')
|
||||
export class SamlController {
|
||||
constructor(
|
||||
|
@ -41,8 +39,7 @@ export class SamlController {
|
|||
private readonly internalHooks: InternalHooks,
|
||||
) {}
|
||||
|
||||
@NoAuthRequired()
|
||||
@Get(SamlUrls.metadata)
|
||||
@Get(SamlUrls.metadata, { skipAuth: true })
|
||||
async getServiceProviderMetadata(_: express.Request, res: express.Response) {
|
||||
return res
|
||||
.header('Content-Type', 'text/xml')
|
||||
|
@ -53,7 +50,6 @@ export class SamlController {
|
|||
* GET /sso/saml/config
|
||||
* Return SAML config
|
||||
*/
|
||||
@Authorized('any')
|
||||
@Get(SamlUrls.config, { middlewares: [samlLicensedMiddleware] })
|
||||
async configGet() {
|
||||
const prefs = this.samlService.samlPreferences;
|
||||
|
@ -101,8 +97,7 @@ export class SamlController {
|
|||
* GET /sso/saml/acs
|
||||
* Assertion Consumer Service endpoint
|
||||
*/
|
||||
@NoAuthRequired()
|
||||
@Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] })
|
||||
@Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true })
|
||||
async acsGet(req: SamlConfiguration.AcsRequest, res: express.Response) {
|
||||
return await this.acsHandler(req, res, 'redirect');
|
||||
}
|
||||
|
@ -111,8 +106,7 @@ export class SamlController {
|
|||
* POST /sso/saml/acs
|
||||
* Assertion Consumer Service endpoint
|
||||
*/
|
||||
@NoAuthRequired()
|
||||
@Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] })
|
||||
@Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true })
|
||||
async acsPost(req: SamlConfiguration.AcsRequest, res: express.Response) {
|
||||
return await this.acsHandler(req, res, 'post');
|
||||
}
|
||||
|
@ -177,8 +171,7 @@ export class SamlController {
|
|||
* Access URL for implementing SP-init SSO
|
||||
* This endpoint is available if SAML is licensed and enabled
|
||||
*/
|
||||
@NoAuthRequired()
|
||||
@Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware] })
|
||||
@Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware], skipAuth: true })
|
||||
async initSsoGet(req: express.Request, res: express.Response) {
|
||||
let redirectUrl = '';
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Authorized, RestController, Get, Middleware } from '@/decorators';
|
||||
import { RestController, Get, Middleware } from '@/decorators';
|
||||
import { WorkflowHistoryRequest } from '@/requests';
|
||||
import { WorkflowHistoryService } from './workflowHistory.service.ee';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
@ -11,7 +11,6 @@ import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-v
|
|||
|
||||
const DEFAULT_TAKE = 20;
|
||||
|
||||
@Authorized()
|
||||
@RestController('/workflow-history')
|
||||
export class WorkflowHistoryController {
|
||||
constructor(private readonly historyService: WorkflowHistoryService) {}
|
||||
|
|
|
@ -8,7 +8,7 @@ import * as ResponseHelper from '@/ResponseHelper';
|
|||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||
import type { IWorkflowResponse } from '@/Interfaces';
|
||||
import config from '@/config';
|
||||
import { Authorized, Delete, Get, Patch, Post, Put, RestController } from '@/decorators';
|
||||
import { Delete, Get, Patch, Post, Put, RestController } from '@/decorators';
|
||||
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
|
@ -39,7 +39,6 @@ import { WorkflowExecutionService } from './workflowExecution.service';
|
|||
import { WorkflowSharingService } from './workflowSharing.service';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/workflows')
|
||||
export class WorkflowsController {
|
||||
constructor(
|
||||
|
|
|
@ -17,14 +17,12 @@ describe('Auth Middleware', () => {
|
|||
['PATCH', '/me'],
|
||||
['PATCH', '/me/password'],
|
||||
['POST', '/me/survey'],
|
||||
['POST', '/owner/setup'],
|
||||
];
|
||||
|
||||
/** Routes requiring a valid `n8n-auth` cookie for an owner. */
|
||||
const ROUTES_REQUIRING_AUTHORIZATION: Readonly<Array<[string, string]>> = [
|
||||
['POST', '/invitations'],
|
||||
['DELETE', '/users/123'],
|
||||
['POST', '/owner/setup'],
|
||||
];
|
||||
|
||||
describe('Routes requiring Authentication', () => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import validator from 'validator';
|
||||
import type { SuperAgentTest } from 'supertest';
|
||||
|
||||
import config from '@/config';
|
||||
import type { User } from '@db/entities/User';
|
||||
|
@ -18,11 +17,9 @@ import Container from 'typedi';
|
|||
const testServer = utils.setupTestServer({ endpointGroups: ['owner'] });
|
||||
|
||||
let ownerShell: User;
|
||||
let authOwnerShellAgent: SuperAgentTest;
|
||||
|
||||
beforeEach(async () => {
|
||||
ownerShell = await createUserShell('global:owner');
|
||||
authOwnerShellAgent = testServer.authAgentFor(ownerShell);
|
||||
config.set('userManagement.isInstanceOwnerSetUp', false);
|
||||
});
|
||||
|
||||
|
@ -39,7 +36,7 @@ describe('POST /owner/setup', () => {
|
|||
password: randomValidPassword(),
|
||||
};
|
||||
|
||||
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
|
||||
const response = await testServer.authlessAgent.post('/owner/setup').send(newOwnerData);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
|
@ -88,7 +85,7 @@ describe('POST /owner/setup', () => {
|
|||
password: randomValidPassword(),
|
||||
};
|
||||
|
||||
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
|
||||
const response = await testServer.authlessAgent.post('/owner/setup').send(newOwnerData);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
|
@ -150,7 +147,7 @@ describe('POST /owner/setup', () => {
|
|||
|
||||
test('should fail with invalid inputs', async () => {
|
||||
for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) {
|
||||
const response = await authOwnerShellAgent.post('/owner/setup').send(invalidPayload);
|
||||
const response = await testServer.authlessAgent.post('/owner/setup').send(invalidPayload);
|
||||
expect(response.statusCode).toBe(400);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('AuthService', () => {
|
|||
config.set('userManagement.jwtRefreshTimeoutHours', 0);
|
||||
});
|
||||
|
||||
describe('createAuthMiddleware', () => {
|
||||
describe('authMiddleware', () => {
|
||||
const req = mock<AuthenticatedRequest>({ cookies: {}, user: undefined });
|
||||
const res = mock<Response>();
|
||||
const next = jest.fn() as NextFunction;
|
||||
|
@ -48,64 +48,34 @@ describe('AuthService', () => {
|
|||
res.status.mockReturnThis();
|
||||
});
|
||||
|
||||
describe('authRole = none', () => {
|
||||
const authMiddleware = authService.createAuthMiddleware('none');
|
||||
|
||||
it('should just skips auth checks', async () => {
|
||||
await authMiddleware(req, res, next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should 401 if no cookie is set', async () => {
|
||||
req.cookies[AUTH_COOKIE_NAME] = undefined;
|
||||
await authService.authMiddleware(req, res, next);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
describe('authRole = any', () => {
|
||||
const authMiddleware = authService.createAuthMiddleware('any');
|
||||
it('should 401 and clear the cookie if the JWT is expired', async () => {
|
||||
req.cookies[AUTH_COOKIE_NAME] = validToken;
|
||||
jest.advanceTimersByTime(365 * Time.days.toMilliseconds);
|
||||
|
||||
it('should 401 if no cookie is set', async () => {
|
||||
req.cookies[AUTH_COOKIE_NAME] = undefined;
|
||||
await authMiddleware(req, res, next);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('should 401 and clear the cookie if the JWT is expired', async () => {
|
||||
req.cookies[AUTH_COOKIE_NAME] = validToken;
|
||||
jest.advanceTimersByTime(365 * Time.days.toMilliseconds);
|
||||
|
||||
await authMiddleware(req, res, next);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.clearCookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME);
|
||||
});
|
||||
|
||||
it('should refresh the cookie before it expires', async () => {
|
||||
req.cookies[AUTH_COOKIE_NAME] = validToken;
|
||||
jest.advanceTimersByTime(6 * Time.days.toMilliseconds);
|
||||
userRepository.findOne.mockResolvedValue(user);
|
||||
|
||||
await authMiddleware(req, res, next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.cookie).toHaveBeenCalledWith('n8n-auth', expect.any(String), {
|
||||
httpOnly: true,
|
||||
maxAge: 604800000,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
});
|
||||
await authService.authMiddleware(req, res, next);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.clearCookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME);
|
||||
});
|
||||
|
||||
describe('authRole = global:owner', () => {
|
||||
const authMiddleware = authService.createAuthMiddleware('global:owner');
|
||||
it('should refresh the cookie before it expires', async () => {
|
||||
req.cookies[AUTH_COOKIE_NAME] = validToken;
|
||||
jest.advanceTimersByTime(6 * Time.days.toMilliseconds);
|
||||
userRepository.findOne.mockResolvedValue(user);
|
||||
|
||||
it('should 403 if the user does not have the correct role', async () => {
|
||||
req.cookies[AUTH_COOKIE_NAME] = validToken;
|
||||
jest.advanceTimersByTime(6 * Time.days.toMilliseconds);
|
||||
userRepository.findOne.mockResolvedValue(
|
||||
mock<User>({ ...userData, role: 'global:member' }),
|
||||
);
|
||||
|
||||
await authMiddleware(req, res, next);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
await authService.authMiddleware(req, res, next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.cookie).toHaveBeenCalledWith('n8n-auth', expect.any(String), {
|
||||
httpOnly: true,
|
||||
maxAge: 604800000,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,11 +93,15 @@ describe('OwnerController', () => {
|
|||
});
|
||||
const res = mock<Response>();
|
||||
configGetSpy.mockReturnValue(false);
|
||||
userRepository.findOneOrFail.calledWith(anyObject()).mockResolvedValue(user);
|
||||
userRepository.save.calledWith(anyObject()).mockResolvedValue(user);
|
||||
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
|
||||
|
||||
await controller.setupOwner(req, res);
|
||||
|
||||
expect(userRepository.findOneOrFail).toHaveBeenCalledWith({
|
||||
where: { role: 'global:owner' },
|
||||
});
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false });
|
||||
expect(authService.issueCookie).toHaveBeenCalledWith(res, user);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue