mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Add onlySharedWithMe filter to GET /credentials endpoint (no-changelog) (#14885)
Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
@@ -19,4 +19,6 @@ export class CredentialsGetManyRequestQuery extends Z.class({
|
|||||||
* This switches `includeScopes` to true to be able to check for the scopes
|
* This switches `includeScopes` to true to be able to check for the scopes
|
||||||
*/
|
*/
|
||||||
includeData: booleanFromString.optional(),
|
includeData: booleanFromString.optional(),
|
||||||
|
|
||||||
|
onlySharedWithMe: booleanFromString.optional(),
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export class CredentialsController {
|
|||||||
listQueryOptions: req.listQueryOptions,
|
listQueryOptions: req.listQueryOptions,
|
||||||
includeScopes: query.includeScopes,
|
includeScopes: query.includeScopes,
|
||||||
includeData: query.includeData,
|
includeData: query.includeData,
|
||||||
|
onlySharedWithMe: query.onlySharedWithMe,
|
||||||
});
|
});
|
||||||
credentials.forEach((c) => {
|
credentials.forEach((c) => {
|
||||||
// @ts-expect-error: This is to emulate the old behavior of removing the shared
|
// @ts-expect-error: This is to emulate the old behavior of removing the shared
|
||||||
|
|||||||
@@ -72,10 +72,12 @@ export class CredentialsService {
|
|||||||
listQueryOptions = {},
|
listQueryOptions = {},
|
||||||
includeScopes = false,
|
includeScopes = false,
|
||||||
includeData = false,
|
includeData = false,
|
||||||
|
onlySharedWithMe = false,
|
||||||
}: {
|
}: {
|
||||||
listQueryOptions?: ListQuery.Options & { includeData?: boolean };
|
listQueryOptions?: ListQuery.Options & { includeData?: boolean };
|
||||||
includeScopes?: boolean;
|
includeScopes?: boolean;
|
||||||
includeData?: boolean;
|
includeData?: boolean;
|
||||||
|
onlySharedWithMe?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const returnAll = user.hasGlobalScope('credential:list');
|
const returnAll = user.hasGlobalScope('credential:list');
|
||||||
@@ -85,6 +87,14 @@ export class CredentialsService {
|
|||||||
? listQueryOptions.filter.projectId
|
? listQueryOptions.filter.projectId
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
if (onlySharedWithMe) {
|
||||||
|
listQueryOptions.filter = {
|
||||||
|
...listQueryOptions.filter,
|
||||||
|
withRole: 'credential:user',
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (includeData) {
|
if (includeData) {
|
||||||
// We need the scopes to check if we're allowed to include the decrypted
|
// We need the scopes to check if we're allowed to include the decrypted
|
||||||
// data.
|
// data.
|
||||||
@@ -118,7 +128,10 @@ export class CredentialsService {
|
|||||||
// it's shared to a project, it won't be able to find the home project.
|
// it's shared to a project, it won't be able to find the home project.
|
||||||
// To solve this, we have to get all the relation now, even though
|
// To solve this, we have to get all the relation now, even though
|
||||||
// we're deleting them later.
|
// we're deleting them later.
|
||||||
if ((listQueryOptions.filter?.shared as { projectId?: string })?.projectId) {
|
if (
|
||||||
|
(listQueryOptions.filter?.shared as { projectId?: string })?.projectId ??
|
||||||
|
onlySharedWithMe
|
||||||
|
) {
|
||||||
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
||||||
credentials.map((c) => c.id),
|
credentials.map((c) => c.id),
|
||||||
);
|
);
|
||||||
@@ -168,7 +181,10 @@ export class CredentialsService {
|
|||||||
// it's shared to a project, it won't be able to find the home project.
|
// it's shared to a project, it won't be able to find the home project.
|
||||||
// To solve this, we have to get all the relation now, even though
|
// To solve this, we have to get all the relation now, even though
|
||||||
// we're deleting them later.
|
// we're deleting them later.
|
||||||
if ((listQueryOptions.filter?.shared as { projectId?: string })?.projectId) {
|
if (
|
||||||
|
(listQueryOptions.filter?.shared as { projectId?: string })?.projectId ??
|
||||||
|
onlySharedWithMe
|
||||||
|
) {
|
||||||
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(
|
||||||
credentials.map((c) => c.id),
|
credentials.map((c) => c.id),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { User } from '@n8n/db';
|
||||||
import { CredentialsEntity } from '@n8n/db';
|
import { CredentialsEntity } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { DataSource, In, Repository, Like } from '@n8n/typeorm';
|
import { DataSource, In, Repository, Like } from '@n8n/typeorm';
|
||||||
@@ -19,7 +20,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findMany(
|
async findMany(
|
||||||
listQueryOptions?: ListQuery.Options & { includeData?: boolean },
|
listQueryOptions?: ListQuery.Options & { includeData?: boolean; user?: User },
|
||||||
credentialIds?: string[],
|
credentialIds?: string[],
|
||||||
) {
|
) {
|
||||||
const findManyOptions = this.toFindManyOptions(listQueryOptions);
|
const findManyOptions = this.toFindManyOptions(listQueryOptions);
|
||||||
@@ -36,7 +37,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
|||||||
|
|
||||||
type Select = Array<keyof CredentialsEntity>;
|
type Select = Array<keyof CredentialsEntity>;
|
||||||
|
|
||||||
const defaultRelations = ['shared', 'shared.project'];
|
const defaultRelations = ['shared', 'shared.project', 'shared.project.projectRelations'];
|
||||||
const defaultSelect: Select = ['id', 'name', 'type', 'isManaged', 'createdAt', 'updatedAt'];
|
const defaultSelect: Select = ['id', 'name', 'type', 'isManaged', 'createdAt', 'updatedAt'];
|
||||||
|
|
||||||
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };
|
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };
|
||||||
@@ -99,6 +100,23 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
|||||||
};
|
};
|
||||||
delete filter.withRole;
|
delete filter.withRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filter.user &&
|
||||||
|
typeof filter.user === 'object' &&
|
||||||
|
'id' in filter.user &&
|
||||||
|
typeof filter.user.id === 'string'
|
||||||
|
) {
|
||||||
|
filter.shared = {
|
||||||
|
...(filter?.shared ? filter.shared : {}),
|
||||||
|
project: {
|
||||||
|
projectRelations: {
|
||||||
|
userId: filter.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
delete filter.user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
|
async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
shareCredentialWithUsers,
|
shareCredentialWithUsers,
|
||||||
} from '../shared/db/credentials';
|
} from '../shared/db/credentials';
|
||||||
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
||||||
import { createManyUsers, createMember, createOwner } from '../shared/db/users';
|
import { createAdmin, createManyUsers, createMember, createOwner } from '../shared/db/users';
|
||||||
import {
|
import {
|
||||||
randomCredentialPayload as payload,
|
randomCredentialPayload as payload,
|
||||||
randomCredentialPayload,
|
randomCredentialPayload,
|
||||||
@@ -42,6 +42,7 @@ const testServer = setupTestServer({ endpointGroups: ['credentials'] });
|
|||||||
|
|
||||||
let owner: User;
|
let owner: User;
|
||||||
let member: User;
|
let member: User;
|
||||||
|
let admin: User;
|
||||||
let secondMember: User;
|
let secondMember: User;
|
||||||
|
|
||||||
let ownerPersonalProject: Project;
|
let ownerPersonalProject: Project;
|
||||||
@@ -49,6 +50,7 @@ let memberPersonalProject: Project;
|
|||||||
|
|
||||||
let authOwnerAgent: SuperAgentTest;
|
let authOwnerAgent: SuperAgentTest;
|
||||||
let authMemberAgent: SuperAgentTest;
|
let authMemberAgent: SuperAgentTest;
|
||||||
|
let authAdminAgent: SuperAgentTest;
|
||||||
|
|
||||||
let projectRepository: ProjectRepository;
|
let projectRepository: ProjectRepository;
|
||||||
let sharedCredentialsRepository: SharedCredentialsRepository;
|
let sharedCredentialsRepository: SharedCredentialsRepository;
|
||||||
@@ -58,6 +60,7 @@ beforeEach(async () => {
|
|||||||
|
|
||||||
owner = await createOwner();
|
owner = await createOwner();
|
||||||
member = await createMember();
|
member = await createMember();
|
||||||
|
admin = await createAdmin();
|
||||||
secondMember = await createMember();
|
secondMember = await createMember();
|
||||||
|
|
||||||
ownerPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
ownerPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
||||||
@@ -69,6 +72,7 @@ beforeEach(async () => {
|
|||||||
|
|
||||||
authOwnerAgent = testServer.authAgentFor(owner);
|
authOwnerAgent = testServer.authAgentFor(owner);
|
||||||
authMemberAgent = testServer.authAgentFor(member);
|
authMemberAgent = testServer.authAgentFor(member);
|
||||||
|
authAdminAgent = testServer.authAgentFor(admin);
|
||||||
|
|
||||||
projectRepository = Container.get(ProjectRepository);
|
projectRepository = Container.get(ProjectRepository);
|
||||||
sharedCredentialsRepository = Container.get(SharedCredentialsRepository);
|
sharedCredentialsRepository = Container.get(SharedCredentialsRepository);
|
||||||
@@ -1509,6 +1513,103 @@ describe('POST /credentials/test', () => {
|
|||||||
data: credential.data,
|
data: credential.data,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return only credentials shared with me when ?onlySharedWithMe=true (owner)', async () => {
|
||||||
|
await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: owner,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: owner,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberCredential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: member,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
await shareCredentialWithUsers(memberCredential, [owner]);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await authOwnerAgent.get('/credentials').query({ onlySharedWithMe: true });
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data).toHaveLength(1);
|
||||||
|
expect(response.body.data[0].id).toBe(memberCredential.id);
|
||||||
|
expect(response.body.data[0].homeProject).not.toBeNull();
|
||||||
|
|
||||||
|
response = await authMemberAgent.get('/credentials').query({ onlySharedWithMe: true });
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return only credentials shared with me when ?onlySharedWithMe=true (admin)', async () => {
|
||||||
|
await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: admin,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: admin,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberCredential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: member,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
await shareCredentialWithUsers(memberCredential, [admin]);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await authAdminAgent.get('/credentials').query({ onlySharedWithMe: true });
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data).toHaveLength(1);
|
||||||
|
expect(response.body.data[0].id).toBe(memberCredential.id);
|
||||||
|
expect(response.body.data[0].homeProject).not.toBeNull();
|
||||||
|
|
||||||
|
response = await authMemberAgent.get('/credentials').query({ onlySharedWithMe: true });
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return only credentials shared with me when ?onlySharedWithMe=true (member)', async () => {
|
||||||
|
await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: member,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: member,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ownerCredential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: owner,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
await shareCredentialWithUsers(ownerCredential, [member]);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await authMemberAgent.get('/credentials').query({ onlySharedWithMe: true });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(1);
|
||||||
|
expect(response.body.data[0].id).toBe(ownerCredential.id);
|
||||||
|
expect(response.body.data[0].homeProject).not.toBeNull();
|
||||||
|
|
||||||
|
response = await authOwnerAgent.get('/credentials').query({ onlySharedWithMe: true });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const INVALID_PAYLOADS = [
|
const INVALID_PAYLOADS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user