feat: Replace owner checks with scope checks (no-changelog) (#7846)

Github issue / Community forum post (link here to close automatically):
This commit is contained in:
Val
2023-11-29 14:48:36 +00:00
committed by GitHub
parent d5762a7539
commit 1cb92ffe16
26 changed files with 136 additions and 78 deletions

View File

@@ -50,7 +50,7 @@ EECredentialsController.get(
const userSharing = credential.shared?.find((shared) => shared.user.id === req.user.id);
if (!userSharing && req.user.globalRole.name !== 'owner') {
if (!userSharing && !(await req.user.hasGlobalScope('credential:read'))) {
throw new UnauthorizedError('Forbidden.');
}
@@ -82,7 +82,10 @@ EECredentialsController.post(
const credentialId = credentials.id;
const { ownsCredential } = await EECredentials.isOwned(req.user, credentialId);
const sharing = await EECredentials.getSharing(req.user, credentialId);
const sharing = await EECredentials.getSharing(req.user, credentialId, {
allowGlobalScope: true,
globalScope: 'credential:read',
});
if (!ownsCredential) {
if (!sharing) {
throw new UnauthorizedError('Forbidden');

View File

@@ -58,7 +58,12 @@ credentialsController.get(
const { id: credentialId } = req.params;
const includeDecryptedData = req.query.includeData === 'true';
const sharing = await CredentialsService.getSharing(req.user, credentialId, ['credentials']);
const sharing = await CredentialsService.getSharing(
req.user,
credentialId,
{ allowGlobalScope: true, globalScope: 'credential:read' },
['credentials'],
);
if (!sharing) {
throw new NotFoundError(`Credential with ID "${credentialId}" could not be found.`);
@@ -91,7 +96,10 @@ credentialsController.post(
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
const { credentials } = req.body;
const sharing = await CredentialsService.getSharing(req.user, credentials.id);
const sharing = await CredentialsService.getSharing(req.user, credentials.id, {
allowGlobalScope: true,
globalScope: 'credential:read',
});
const mergedCredentials = deepCopy(credentials);
if (mergedCredentials.data && sharing?.credentials) {
@@ -134,7 +142,10 @@ credentialsController.patch(
ResponseHelper.send(async (req: CredentialRequest.Update): Promise<ICredentialsDb> => {
const { id: credentialId } = req.params;
const sharing = await CredentialsService.getSharing(req.user, credentialId);
const sharing = await CredentialsService.getSharing(req.user, credentialId, {
allowGlobalScope: true,
globalScope: 'credential:update',
});
if (!sharing) {
Container.get(Logger).info(
@@ -184,7 +195,10 @@ credentialsController.delete(
ResponseHelper.send(async (req: CredentialRequest.Delete) => {
const { id: credentialId } = req.params;
const sharing = await CredentialsService.getSharing(req.user, credentialId);
const sharing = await CredentialsService.getSharing(req.user, credentialId, {
allowGlobalScope: true,
globalScope: 'credential:delete',
});
if (!sharing) {
Container.get(Logger).info(

View File

@@ -4,7 +4,7 @@ import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { SharedCredentials } from '@db/entities/SharedCredentials';
import type { User } from '@db/entities/User';
import { UserService } from '@/services/user.service';
import { CredentialsService } from './credentials.service';
import { CredentialsService, type CredentialsGetSharedOptions } from './credentials.service';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
@@ -14,9 +14,10 @@ export class EECredentialsService extends CredentialsService {
user: User,
credentialId: string,
): Promise<{ ownsCredential: boolean; credential?: CredentialsEntity }> {
const sharing = await this.getSharing(user, credentialId, ['credentials', 'role'], {
allowGlobalOwner: false,
});
const sharing = await this.getSharing(user, credentialId, { allowGlobalScope: false }, [
'credentials',
'role',
]);
if (!sharing || sharing.role.name !== 'owner') return { ownsCredential: false };
@@ -31,15 +32,15 @@ export class EECredentialsService extends CredentialsService {
static async getSharing(
user: User,
credentialId: string,
options: CredentialsGetSharedOptions,
relations: string[] = ['credentials'],
{ allowGlobalOwner } = { allowGlobalOwner: true },
): Promise<SharedCredentials | null> {
const where: FindOptionsWhere<SharedCredentials> = { credentialsId: credentialId };
// Omit user from where if the requesting user is the global
// owner. This allows the global owner to view and delete
// credentials they don't own.
if (!allowGlobalOwner || user.globalRole.name !== 'owner') {
// Omit user from where if the requesting user has relevant
// global credential permissions. This allows the user to
// access credentials they don't own.
if (!options.allowGlobalScope || !(await user.hasGlobalScope(options.globalScope))) {
where.userId = user.id;
}

View File

@@ -11,6 +11,8 @@ import { Container } from 'typedi';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { In, Like } from 'typeorm';
import type { Scope } from '@n8n/permissions';
import * as Db from '@/Db';
import type { ICredentialsDb } from '@/Interfaces';
import { CredentialsHelper, createCredentialsFromCredentialsEntity } from '@/CredentialsHelper';
@@ -28,6 +30,10 @@ import { Logger } from '@/Logger';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
export type CredentialsGetSharedOptions =
| { allowGlobalScope: true; globalScope: Scope }
| { allowGlobalScope: false };
export class CredentialsService {
static async get(
where: FindOptionsWhere<ICredentialsDb>,
@@ -86,7 +92,7 @@ export class CredentialsService {
) {
const findManyOptions = this.toFindManyOptions(options.listQueryOptions);
const returnAll = user.globalRole.name === 'owner' && !options.onlyOwn;
const returnAll = (await user.hasGlobalScope('credential:list')) && !options.onlyOwn;
const isDefaultSelect = !options.listQueryOptions?.select;
if (returnAll) {
@@ -136,15 +142,15 @@ export class CredentialsService {
static async getSharing(
user: User,
credentialId: string,
options: CredentialsGetSharedOptions,
relations: string[] = ['credentials'],
{ allowGlobalOwner } = { allowGlobalOwner: true },
): Promise<SharedCredentials | null> {
const where: FindOptionsWhere<SharedCredentials> = { credentialsId: credentialId };
// Omit user from where if the requesting user is the global
// owner. This allows the global owner to view and delete
// credentials they don't own.
if (!allowGlobalOwner || user.globalRole.name !== 'owner') {
// Omit user from where if the requesting user has relevant
// global credential permissions. This allows the user to
// access credentials they don't own.
if (!options.allowGlobalScope || !(await user.hasGlobalScope(options.globalScope))) {
Object.assign(where, {
userId: user.id,
role: { name: 'owner' },