Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add initial scope checks via decorators #7737

Merged
merged 18 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions packages/@n8n/permissions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ export type Resource =
| 'credential'
| 'variable'
| 'sourceControl'
| 'externalSecretsStore';
| 'externalSecretsProvider'
| 'externalSecret'
| 'eventBusEvent'
| 'eventBusDestination'
| 'orchestration'
| 'communityPackage'
| 'ldap'
| 'saml';

export type ResourceScope<
R extends Resource,
Expand All @@ -16,14 +23,27 @@ export type WildcardScope = `${Resource}:*` | '*';

export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share'>;
export type TagScope = ResourceScope<'tag'>;
export type UserScope = ResourceScope<'user'>;
export type CredentialScope = ResourceScope<'credential'>;
export type UserScope = ResourceScope<'user', DefaultOperations | 'resetPassword'>;
export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share'>;
export type VariableScope = ResourceScope<'variable'>;
export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>;
export type ExternalSecretStoreScope = ResourceScope<
'externalSecretsStore',
DefaultOperations | 'refresh'
export type ExternalSecretProviderScope = ResourceScope<
'externalSecretsProvider',
DefaultOperations | 'sync'
>;
export type ExternalSecretScope = ResourceScope<'externalSecret', 'list'>;
ivov marked this conversation as resolved.
Show resolved Hide resolved
export type EventBusEventScope = ResourceScope<'eventBusEvent', DefaultOperations | 'query'>;
export type EventBusDestinationScope = ResourceScope<
'eventBusDestination',
DefaultOperations | 'test'
>;
export type OrchestrationScope = ResourceScope<'orchestration', 'read' | 'list'>;
export type CommunityPackageScope = ResourceScope<
'communityPackage',
'install' | 'uninstall' | 'update' | 'list'
>;
export type LdapScope = ResourceScope<'ldap', 'manage' | 'sync'>;
export type SamlScope = ResourceScope<'saml', 'manage'>;

export type Scope =
| WorkflowScope
Expand All @@ -32,7 +52,14 @@ export type Scope =
| CredentialScope
| VariableScope
| SourceControlScope
| ExternalSecretStoreScope;
| ExternalSecretProviderScope
| ExternalSecretScope
| EventBusEventScope
| EventBusDestinationScope
| OrchestrationScope
| CommunityPackageScope
| LdapScope
| SamlScope;

export type ScopeLevel<T extends 'global' | 'project' | 'resource'> = Record<T, Scope[]>;
export type GlobalScopes = ScopeLevel<'global'>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { Authorized, Get, Post, RestController } from '@/decorators';
import { Authorized, Get, Post, RestController, RequireGlobalScope } from '@/decorators';
import { ExternalSecretsRequest } from '@/requests';
import { NotFoundError } from '@/ResponseHelper';
import { Response } from 'express';
import { Service } from 'typedi';
import { ProviderNotFoundError, ExternalSecretsService } from './ExternalSecrets.service.ee';

@Service()
@Authorized(['global', 'owner'])
@Authorized()
@RestController('/external-secrets')
export class ExternalSecretsController {
constructor(private readonly secretsService: ExternalSecretsService) {}

@Get('/providers')
@RequireGlobalScope('externalSecretsProvider:list')
async getProviders() {
return this.secretsService.getProviders();
}

@Get('/providers/:provider')
@RequireGlobalScope('externalSecretsProvider:read')
async getProvider(req: ExternalSecretsRequest.GetProvider) {
const providerName = req.params.provider;
try {
Expand All @@ -30,6 +32,7 @@ export class ExternalSecretsController {
}

@Post('/providers/:provider/test')
@RequireGlobalScope('externalSecretsProvider:read')
async testProviderSettings(req: ExternalSecretsRequest.TestProviderSettings, res: Response) {
const providerName = req.params.provider;
try {
Expand All @@ -49,6 +52,7 @@ export class ExternalSecretsController {
}

@Post('/providers/:provider')
@RequireGlobalScope('externalSecretsProvider:create')
async setProviderSettings(req: ExternalSecretsRequest.SetProviderSettings) {
const providerName = req.params.provider;
try {
Expand All @@ -63,6 +67,7 @@ export class ExternalSecretsController {
}

@Post('/providers/:provider/connect')
@RequireGlobalScope('externalSecretsProvider:update')
async setProviderConnected(req: ExternalSecretsRequest.SetProviderConnected) {
const providerName = req.params.provider;
try {
Expand All @@ -77,6 +82,7 @@ export class ExternalSecretsController {
}

@Post('/providers/:provider/update')
@RequireGlobalScope('externalSecretsProvider:sync')
async updateProvider(req: ExternalSecretsRequest.UpdateProvider, res: Response) {
const providerName = req.params.provider;
try {
Expand All @@ -96,6 +102,7 @@ export class ExternalSecretsController {
}

@Get('/secrets')
@RequireGlobalScope('externalSecret:list')
getSecretNames() {
return this.secretsService.getAllSecrets();
}
Expand Down
9 changes: 2 additions & 7 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ import { License } from './License';
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
import { SamlController } from './sso/saml/routes/saml.controller.ee';
import { SamlService } from './sso/saml/saml.service.ee';
import { variablesController } from './environments/variables/variables.controller';
import { VariablesController } from './environments/variables/variables.controller.ee';
import { LdapManager } from './Ldap/LdapManager.ee';
import {
isLdapCurrentAuthenticationMethod,
Expand Down Expand Up @@ -282,6 +282,7 @@ export class Server extends AbstractServer {
Container.get(OrchestrationController),
Container.get(WorkflowHistoryController),
Container.get(BinaryDataController),
Container.get(VariablesController),
new InvitationController(
config,
logger,
Expand Down Expand Up @@ -419,12 +420,6 @@ export class Server extends AbstractServer {
this.logger.warn(`SAML initialization failed: ${error.message}`);
}

// ----------------------------------------
// Variables
// ----------------------------------------

this.app.use(`/${this.restEndpoint}/variables`, variablesController);

// ----------------------------------------
// Source Control
// ----------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { RoleRepository } from '@db/repositories/role.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { RoleService } from './services/role.service';
import { VariablesService } from './environments/variables/variables.service';
import { VariablesService } from './environments/variables/variables.service.ee';
import { Logger } from './Logger';

const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
Expand Down
17 changes: 15 additions & 2 deletions packages/cli/src/controllers/communityPackages.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ import {
STARTER_TEMPLATE_NAME,
UNKNOWN_FAILURE_REASON,
} from '@/constants';
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import {
Authorized,
Delete,
Get,
Middleware,
Patch,
Post,
RestController,
RequireGlobalScope,
} from '@/decorators';
import { NodeRequest } from '@/requests';
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
Expand All @@ -33,7 +42,7 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
}

@Service()
@Authorized(['global', 'owner'])
@Authorized()
@RestController('/community-packages')
export class CommunityPackagesController {
constructor(
Expand All @@ -54,6 +63,7 @@ export class CommunityPackagesController {
}

@Post('/')
@RequireGlobalScope('communityPackage:install')
async installPackage(req: NodeRequest.Post) {
const { name } = req.body;

Expand Down Expand Up @@ -150,6 +160,7 @@ export class CommunityPackagesController {
}

@Get('/')
@RequireGlobalScope('communityPackage:list')
async getInstalledPackages() {
const installedPackages = await this.communityPackagesService.getAllInstalledPackages();

Expand Down Expand Up @@ -184,6 +195,7 @@ export class CommunityPackagesController {
}

@Delete('/')
@RequireGlobalScope('communityPackage:uninstall')
async uninstallPackage(req: NodeRequest.Delete) {
const { name } = req.query;

Expand Down Expand Up @@ -235,6 +247,7 @@ export class CommunityPackagesController {
}

@Patch('/')
@RequireGlobalScope('communityPackage:update')
async updatePackage(req: NodeRequest.Update) {
const { name } = req.body;

Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/controllers/invitation.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { In } from 'typeorm';
import Container, { Service } from 'typedi';
import { Authorized, NoAuthRequired, Post, RestController } from '@/decorators';
import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators';
import { BadRequestError, UnauthorizedError } from '@/ResponseHelper';
import { issueCookie } from '@/auth/jwt';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
Expand All @@ -18,6 +18,7 @@ import type { User } from '@/databases/entities/User';
import validator from 'validator';

@Service()
@Authorized()
ivov marked this conversation as resolved.
Show resolved Hide resolved
@RestController('/invitations')
export class InvitationController {
constructor(
Expand All @@ -33,8 +34,8 @@ export class InvitationController {
* Send email invite(s) to one or multiple users and create user shell(s).
*/

@Authorized(['global', 'owner'])
@Post('/')
@RequireGlobalScope('user:create')
async inviteUser(req: UserRequest.Invite) {
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();

Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/controllers/ldap.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pick from 'lodash/pick';
import { Authorized, Get, Post, Put, RestController } from '@/decorators';
import { Authorized, Get, Post, Put, RestController, RequireGlobalScope } from '@/decorators';
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
import { LdapService } from '@/Ldap/LdapService.ee';
import { LdapSync } from '@/Ldap/LdapSync.ee';
Expand All @@ -8,7 +8,7 @@ import { BadRequestError } from '@/ResponseHelper';
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
import { InternalHooks } from '@/InternalHooks';

@Authorized(['global', 'owner'])
@Authorized()
@RestController('/ldap')
export class LdapController {
constructor(
Expand All @@ -18,11 +18,13 @@ export class LdapController {
) {}

@Get('/config')
@RequireGlobalScope('ldap:manage')
async getConfig() {
return getLdapConfig();
}

@Post('/test-connection')
@RequireGlobalScope('ldap:manage')
async testConnection() {
try {
await this.ldapService.testConnection();
Expand All @@ -32,6 +34,7 @@ export class LdapController {
}

@Put('/config')
@RequireGlobalScope('ldap:manage')
async updateConfig(req: LdapConfiguration.Update) {
try {
await updateLdapConfig(req.body);
Expand All @@ -50,12 +53,14 @@ export class LdapController {
}

@Get('/sync')
@RequireGlobalScope('ldap:sync')
async getLdapSync(req: LdapConfiguration.GetSync) {
const { page = '0', perPage = '20' } = req.query;
return getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
}

@Post('/sync')
@RequireGlobalScope('ldap:sync')
async syncLdap(req: LdapConfiguration.Sync) {
try {
await this.ldapSync.run(req.body.type);
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/controllers/orchestration.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Authorized, Post, RestController } from '@/decorators';
import { Authorized, Post, RestController, RequireGlobalScope } from '@/decorators';
import { OrchestrationRequest } from '@/requests';
import { Service } from 'typedi';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup';
import { License } from '../License';

@Authorized('any')
@Authorized()
@RestController('/orchestration')
@Service()
export class OrchestrationController {
Expand All @@ -17,19 +17,22 @@ export class OrchestrationController {
* These endpoints do not return anything, they just trigger the messsage to
* the workers to respond on Redis with their status.
*/
@RequireGlobalScope('orchestration:read')
@Post('/worker/status/:id')
async getWorkersStatus(req: OrchestrationRequest.Get) {
if (!this.licenseService.isWorkerViewLicensed()) return;
const id = req.params.id;
return this.singleMainSetup.getWorkerStatus(id);
}

@RequireGlobalScope('orchestration:read')
@Post('/worker/status')
async getWorkersStatusAll() {
if (!this.licenseService.isWorkerViewLicensed()) return;
return this.singleMainSetup.getWorkerStatus();
}

@RequireGlobalScope('orchestration:list')
@Post('/worker/ids')
async getWorkerIdsAll() {
if (!this.licenseService.isWorkerViewLicensed()) return;
Expand Down
16 changes: 14 additions & 2 deletions packages/cli/src/controllers/tags.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { Request, Response, NextFunction } from 'express';
import config from '@/config';
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import {
Authorized,
Delete,
Get,
Middleware,
Patch,
Post,
RestController,
RequireGlobalScope,
} from '@/decorators';
import { TagService } from '@/services/tag.service';
import { BadRequestError } from '@/ResponseHelper';
import { TagsRequest } from '@/requests';
Expand All @@ -23,26 +32,29 @@ export class TagsController {
}

@Get('/')
@RequireGlobalScope('tag:list')
async getAll(req: TagsRequest.GetAll) {
return this.tagService.getAll({ withUsageCount: req.query.withUsageCount === 'true' });
}

@Post('/')
@RequireGlobalScope('tag:create')
async createTag(req: TagsRequest.Create) {
const tag = this.tagService.toEntity({ name: req.body.name });

return this.tagService.save(tag, 'create');
}

@Patch('/:id(\\w+)')
@RequireGlobalScope('tag:update')
async updateTag(req: TagsRequest.Update) {
const newTag = this.tagService.toEntity({ id: req.params.id, name: req.body.name.trim() });

return this.tagService.save(newTag, 'update');
}

@Authorized(['global', 'owner'])
@Delete('/:id(\\w+)')
@RequireGlobalScope('tag:delete')
async deleteTag(req: TagsRequest.Delete) {
const { id } = req.params;

Expand Down
Loading