mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
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:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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 } },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user