Skip to content

Commit

Permalink
feat: Add initial scope checks via decorators (#7737)
Browse files Browse the repository at this point in the history
  • Loading branch information
valya committed Nov 28, 2023
1 parent 753cbc1 commit a37f1cb
Show file tree
Hide file tree
Showing 22 changed files with 233 additions and 89 deletions.
39 changes: 33 additions & 6 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 @@ -17,14 +24,27 @@ export type WildcardScope = `${Resource}:*` | '*';

export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share'>;
export type TagScope = ResourceScope<'tag'>;
export type UserScope = ResourceScope<'user'>;
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'>;
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 @@ -33,7 +53,14 @@ export type Scope =
| CredentialScope
| VariableScope
| SourceControlScope
| ExternalSecretStoreScope;
| ExternalSecretProviderScope
| ExternalSecretScope
| EventBusEventScope
| EventBusDestinationScope
| OrchestrationScope
| CommunityPackageScope
| LdapScope
| SamlScope;

export type ScopeLevel = 'global' | 'project' | 'resource';
export type GetScopeLevel<T extends ScopeLevel> = Record<T, Scope[]>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Authorized, Get, Post, RestController } from '@/decorators';
import { Authorized, Get, Post, RestController, RequireGlobalScope } from '@/decorators';
import { ExternalSecretsRequest } from '@/requests';
import { Response } from 'express';
import { Service } from 'typedi';
Expand All @@ -7,17 +7,19 @@ import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-
import { NotFoundError } from '@/errors/response-errors/not-found.error';

@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 @@ -31,6 +33,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 @@ -50,6 +53,7 @@ export class ExternalSecretsController {
}

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

@Post('/providers/:provider/connect')
@RequireGlobalScope('externalSecretsProvider:update')
async setProviderConnected(req: ExternalSecretsRequest.SetProviderConnected) {
const providerName = req.params.provider;
try {
Expand All @@ -78,6 +83,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 @@ -97,6 +103,7 @@ export class ExternalSecretsController {
}

@Get('/secrets')
@RequireGlobalScope('externalSecret:list')
getSecretNames() {
return this.secretsService.getAllSecrets();
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export class Server extends AbstractServer {
Container.get(OrchestrationController),
Container.get(WorkflowHistoryController),
Container.get(BinaryDataController),
Container.get(VariablesController),
new InvitationController(
config,
logger,
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 type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces';
Expand Down Expand Up @@ -34,7 +43,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 @@ -55,6 +64,7 @@ export class CommunityPackagesController {
}

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

Expand Down Expand Up @@ -151,6 +161,7 @@ export class CommunityPackagesController {
}

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

Expand Down Expand Up @@ -185,6 +196,7 @@ export class CommunityPackagesController {
}

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

Expand Down Expand Up @@ -236,6 +248,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 { issueCookie } from '@/auth/jwt';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { Response } from 'express';
Expand All @@ -19,6 +19,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';

@Service()
@Authorized()
@RestController('/invitations')
export class InvitationController {
constructor(
Expand All @@ -34,8 +35,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 { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
import { InternalHooks } from '@/InternalHooks';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';

@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 { TagsRequest } from '@/requests';
import { Service } from 'typedi';
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

0 comments on commit a37f1cb

Please sign in to comment.