-
Notifications
You must be signed in to change notification settings - Fork 8.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(core): Assign credential ownership correctly in source control im…
…port (#8955)
- Loading branch information
Showing
7 changed files
with
202 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 152 additions & 0 deletions
152
packages/cli/test/integration/environments/source-control-import.service.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import fsp from 'node:fs/promises'; | ||
import Container from 'typedi'; | ||
import { mock } from 'jest-mock-extended'; | ||
import * as utils from 'n8n-workflow'; | ||
import { Cipher } from 'n8n-core'; | ||
import { nanoid } from 'nanoid'; | ||
import type { InstanceSettings } from 'n8n-core'; | ||
|
||
import * as testDb from '../shared/testDb'; | ||
import { SourceControlImportService } from '@/environments/sourceControl/sourceControlImport.service.ee'; | ||
import { createMember, getGlobalOwner } from '../shared/db/users'; | ||
import { SharedCredentialsRepository } from '@/databases/repositories/sharedCredentials.repository'; | ||
import { mockInstance } from '../../shared/mocking'; | ||
import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile'; | ||
import type { ExportableCredential } from '@/environments/sourceControl/types/exportableCredential'; | ||
|
||
describe('SourceControlImportService', () => { | ||
let service: SourceControlImportService; | ||
const cipher = mockInstance(Cipher); | ||
|
||
beforeAll(async () => { | ||
service = new SourceControlImportService( | ||
mock(), | ||
mock(), | ||
mock(), | ||
mock(), | ||
mock<InstanceSettings>({ n8nFolder: '/some-path' }), | ||
); | ||
|
||
await testDb.init(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await testDb.truncate(['Credentials', 'SharedCredentials']); | ||
|
||
jest.restoreAllMocks(); | ||
}); | ||
|
||
afterAll(async () => { | ||
await testDb.terminate(); | ||
}); | ||
|
||
describe('importCredentialsFromWorkFolder()', () => { | ||
describe('if user email specified by `ownedBy` exists at target instance', () => { | ||
it('should assign credential ownership to original user', async () => { | ||
const [importingUser, member] = await Promise.all([getGlobalOwner(), createMember()]); | ||
|
||
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content')); | ||
|
||
const CREDENTIAL_ID = nanoid(); | ||
|
||
const stub: ExportableCredential = { | ||
id: CREDENTIAL_ID, | ||
name: 'My Credential', | ||
type: 'someCredentialType', | ||
data: {}, | ||
nodesAccess: [], | ||
ownedBy: member.email, // user at source instance owns credential | ||
}; | ||
|
||
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub); | ||
|
||
cipher.encrypt.mockReturnValue('some-encrypted-data'); | ||
|
||
await service.importCredentialsFromWorkFolder( | ||
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })], | ||
importingUser.id, | ||
); | ||
|
||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({ | ||
credentialsId: CREDENTIAL_ID, | ||
userId: member.id, | ||
role: 'credential:owner', | ||
}); | ||
|
||
expect(sharing).toBeTruthy(); // same user at target instance owns credential | ||
}); | ||
}); | ||
|
||
describe('if user email specified by `ownedBy` is `null`', () => { | ||
it('should assign credential ownership to importing user', async () => { | ||
const importingUser = await getGlobalOwner(); | ||
|
||
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content')); | ||
|
||
const CREDENTIAL_ID = nanoid(); | ||
|
||
const stub: ExportableCredential = { | ||
id: CREDENTIAL_ID, | ||
name: 'My Credential', | ||
type: 'someCredentialType', | ||
data: {}, | ||
nodesAccess: [], | ||
ownedBy: null, | ||
}; | ||
|
||
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub); | ||
|
||
cipher.encrypt.mockReturnValue('some-encrypted-data'); | ||
|
||
await service.importCredentialsFromWorkFolder( | ||
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })], | ||
importingUser.id, | ||
); | ||
|
||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({ | ||
credentialsId: CREDENTIAL_ID, | ||
userId: importingUser.id, | ||
role: 'credential:owner', | ||
}); | ||
|
||
expect(sharing).toBeTruthy(); // original user has no email, so importing user owns credential | ||
}); | ||
}); | ||
|
||
describe('if user email specified by `ownedBy` does not exist at target instance', () => { | ||
it('should assign credential ownership to importing user', async () => { | ||
const importingUser = await getGlobalOwner(); | ||
|
||
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content')); | ||
|
||
const CREDENTIAL_ID = nanoid(); | ||
|
||
const stub: ExportableCredential = { | ||
id: CREDENTIAL_ID, | ||
name: 'My Credential', | ||
type: 'someCredentialType', | ||
data: {}, | ||
nodesAccess: [], | ||
ownedBy: '[email protected]', // user at source instance owns credential | ||
}; | ||
|
||
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub); | ||
|
||
cipher.encrypt.mockReturnValue('some-encrypted-data'); | ||
|
||
await service.importCredentialsFromWorkFolder( | ||
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })], | ||
importingUser.id, | ||
); | ||
|
||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({ | ||
credentialsId: CREDENTIAL_ID, | ||
userId: importingUser.id, | ||
role: 'credential:owner', | ||
}); | ||
|
||
expect(sharing).toBeTruthy(); // original user missing, so importing user owns credential | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters