feat: Allow instance owners and admins to edit all credentials (#8716)

Co-authored-by: Danny Martini <despair.blue@gmail.com>
This commit is contained in:
Omar Ajoue
2024-02-27 08:26:36 +00:00
committed by GitHub
parent 27f3166272
commit 737170893d
6 changed files with 26 additions and 14 deletions

View File

@@ -74,7 +74,9 @@ export class CredentialsController {
credential = this.ownershipService.addOwnedByAndSharedWith(credential); credential = this.ownershipService.addOwnedByAndSharedWith(credential);
if (!includeDecryptedData || !userSharing || userSharing.role !== 'credential:owner') { // Below, if `userSharing` does not exist, it means this credential is being
// fetched by the instance owner or an admin. In this case, they get the full data
if (!includeDecryptedData || userSharing?.role === 'credential:user') {
const { data: _, ...rest } = credential; const { data: _, ...rest } = credential;
return { ...rest }; return { ...rest };
} }

View File

@@ -4,6 +4,7 @@ export const ownerPermissions: Scope[] = [
'auditLogs:manage', 'auditLogs:manage',
'credential:create', 'credential:create',
'credential:read', 'credential:read',
'credential:update',
'credential:delete', 'credential:delete',
'credential:list', 'credential:list',
'credential:share', 'credential:share',

View File

@@ -229,7 +229,7 @@ describe('GET /credentials/:id', () => {
expect(response2.statusCode).toBe(200); expect(response2.statusCode).toBe(200);
validateMainCredentialData(response2.body.data); validateMainCredentialData(response2.body.data);
expect(response2.body.data.data).toBeUndefined(); expect(response2.body.data.data).toBeDefined(); // Instance owners should be capable of editing all credentials
expect(response2.body.data.sharedWith).toHaveLength(1); expect(response2.body.data.sharedWith).toHaveLength(1);
}); });

View File

@@ -14,6 +14,7 @@ import type { SaveCredentialFunction } from './shared/types';
import * as utils from './shared/utils/'; import * as utils from './shared/utils/';
import { affixRoleToSaveCredential, shareCredentialWithUsers } from './shared/db/credentials'; import { affixRoleToSaveCredential, shareCredentialWithUsers } from './shared/db/credentials';
import { createManyUsers, createUser } from './shared/db/users'; import { createManyUsers, createUser } from './shared/db/users';
import { Credentials } from 'n8n-core';
// mock that credentialsSharing is not enabled // mock that credentialsSharing is not enabled
jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(false); jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(false);
@@ -283,7 +284,7 @@ describe('PATCH /credentials/:id', () => {
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
}); });
test('should not update non-owned cred for owner', async () => { test('should update non-owned cred for owner', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const patchPayload = randomCredentialPayload(); const patchPayload = randomCredentialPayload();
@@ -291,22 +292,29 @@ describe('PATCH /credentials/:id', () => {
.patch(`/credentials/${savedCredential.id}`) .patch(`/credentials/${savedCredential.id}`)
.send(patchPayload); .send(patchPayload);
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(200);
const credential = await Container.get(CredentialsRepository).findOneByOrFail({ const credential = await Container.get(CredentialsRepository).findOneByOrFail({
id: savedCredential.id, id: savedCredential.id,
}); });
expect(credential.name).not.toBe(patchPayload.name); expect(credential.name).toBe(patchPayload.name);
expect(credential.type).not.toBe(patchPayload.type); expect(credential.type).toBe(patchPayload.type);
expect(credential.data).not.toBe(patchPayload.data);
const credentialObject = new Credentials(
{ id: credential.id, name: credential.name },
credential.type,
credential.nodesAccess,
credential.data,
);
expect(credentialObject.getData()).toStrictEqual(patchPayload.data);
const sharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({ const sharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({
relations: ['credentials'], relations: ['credentials'],
where: { credentialsId: credential.id }, where: { credentialsId: credential.id },
}); });
expect(sharedCredential.credentials.name).not.toBe(patchPayload.name); // updated expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
}); });
test('should update owned cred for member', async () => { test('should update owned cred for member', async () => {
@@ -381,7 +389,7 @@ describe('PATCH /credentials/:id', () => {
expect(shellCredential.name).not.toBe(patchPayload.name); // not updated expect(shellCredential.name).not.toBe(patchPayload.name); // not updated
}); });
test('should not update non-owned but shared cred for instance owner', async () => { test('should update non-owned but shared cred for instance owner', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: secondMember }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: secondMember });
await shareCredentialWithUsers(savedCredential, [owner]); await shareCredentialWithUsers(savedCredential, [owner]);
const patchPayload = randomCredentialPayload(); const patchPayload = randomCredentialPayload();
@@ -390,13 +398,13 @@ describe('PATCH /credentials/:id', () => {
.patch(`/credentials/${savedCredential.id}`) .patch(`/credentials/${savedCredential.id}`)
.send(patchPayload); .send(patchPayload);
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(200);
const shellCredential = await Container.get(CredentialsRepository).findOneByOrFail({ const shellCredential = await Container.get(CredentialsRepository).findOneByOrFail({
id: savedCredential.id, id: savedCredential.id,
}); });
expect(shellCredential.name).not.toBe(patchPayload.name); // not updated expect(shellCredential.name).toBe(patchPayload.name); // updated
}); });
test('should fail with invalid inputs', async () => { test('should fail with invalid inputs', async () => {

View File

@@ -6,7 +6,7 @@
:message=" :message="
$locale.baseText( $locale.baseText(
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${ `credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
credentialPermissions.isOwner ? '' : '.sharee' credentialPermissions.update || credentialPermissions.isOwner ? '' : '.sharee'
}`, }`,
{ interpolate: { owner: credentialOwnerName } }, { interpolate: { owner: credentialOwnerName } },
) )
@@ -19,7 +19,7 @@
:message=" :message="
$locale.baseText( $locale.baseText(
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${ `credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
credentialPermissions.isOwner ? '' : '.sharee' credentialPermissions.update || credentialPermissions.isOwner ? '' : '.sharee'
}`, }`,
{ interpolate: { owner: credentialOwnerName } }, { interpolate: { owner: credentialOwnerName } },
) )

View File

@@ -89,7 +89,8 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden
}, },
{ {
name: 'update', name: 'update',
test: (permissions) => !!permissions.isOwner, test: (permissions) =>
hasPermission(['rbac'], { rbac: { scope: 'credential:update' } }) || !!permissions.isOwner,
}, },
{ {
name: 'share', name: 'share',