2025-01-06 01:21:24 -08:00
|
|
|
import { Service } from '@n8n/di';
|
2024-06-24 01:24:05 -07:00
|
|
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
2024-05-17 01:53:15 -07:00
|
|
|
import { In, type EntityManager } from '@n8n/typeorm';
|
2024-09-12 09:07:18 -07:00
|
|
|
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
|
|
|
|
|
|
|
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
|
|
|
import { Project } from '@/databases/entities/project';
|
|
|
|
import { SharedCredentials } from '@/databases/entities/shared-credentials';
|
2024-08-28 08:57:46 -07:00
|
|
|
import type { User } from '@/databases/entities/user';
|
2024-08-27 07:44:32 -07:00
|
|
|
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
2024-05-17 01:53:15 -07:00
|
|
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
2024-09-12 09:07:18 -07:00
|
|
|
import { TransferCredentialError } from '@/errors/response-errors/transfer-credential.error';
|
2024-05-17 01:53:15 -07:00
|
|
|
import { OwnershipService } from '@/services/ownership.service';
|
2024-12-24 04:02:05 -08:00
|
|
|
import { ProjectService } from '@/services/project.service.ee';
|
2024-08-09 03:59:28 -07:00
|
|
|
import { RoleService } from '@/services/role.service';
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2024-09-12 09:07:18 -07:00
|
|
|
import { CredentialsService } from './credentials.service';
|
|
|
|
|
2024-01-31 00:48:48 -08:00
|
|
|
@Service()
|
|
|
|
export class EnterpriseCredentialsService {
|
|
|
|
constructor(
|
|
|
|
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
2024-05-17 01:53:15 -07:00
|
|
|
private readonly ownershipService: OwnershipService,
|
|
|
|
private readonly credentialsService: CredentialsService,
|
2024-06-04 04:54:48 -07:00
|
|
|
private readonly projectService: ProjectService,
|
2024-08-09 03:59:28 -07:00
|
|
|
private readonly roleService: RoleService,
|
2024-01-31 00:48:48 -08:00
|
|
|
) {}
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
async shareWithProjects(
|
2024-08-09 03:59:28 -07:00
|
|
|
user: User,
|
2024-05-17 01:53:15 -07:00
|
|
|
credential: CredentialsEntity,
|
|
|
|
shareWithIds: string[],
|
|
|
|
entityManager?: EntityManager,
|
|
|
|
) {
|
|
|
|
const em = entityManager ?? this.sharedCredentialsRepository.manager;
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const projects = await em.find(Project, {
|
2024-08-09 03:59:28 -07:00
|
|
|
where: [
|
|
|
|
{
|
|
|
|
id: In(shareWithIds),
|
|
|
|
type: 'team',
|
|
|
|
// if user can see all projects, don't check project access
|
|
|
|
// if they can't, find projects they can list
|
|
|
|
...(user.hasGlobalScope('project:list')
|
|
|
|
? {}
|
|
|
|
: {
|
|
|
|
projectRelations: {
|
|
|
|
userId: user.id,
|
|
|
|
role: In(this.roleService.rolesWithScope('project', 'project:list')),
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: In(shareWithIds),
|
|
|
|
type: 'personal',
|
|
|
|
},
|
|
|
|
],
|
2024-05-17 01:53:15 -07:00
|
|
|
});
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2024-08-09 03:59:28 -07:00
|
|
|
const newSharedCredentials = projects.map((project) =>
|
|
|
|
this.sharedCredentialsRepository.create({
|
|
|
|
credentialsId: credential.id,
|
|
|
|
role: 'credential:user',
|
|
|
|
projectId: project.id,
|
|
|
|
}),
|
|
|
|
);
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
return await em.save(newSharedCredentials);
|
2022-09-21 01:20:29 -07:00
|
|
|
}
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
async getOne(user: User, credentialId: string, includeDecryptedData: boolean) {
|
|
|
|
let credential: CredentialsEntity | null = null;
|
|
|
|
let decryptedData: ICredentialDataDecryptedObject | null = null;
|
2022-11-21 23:37:52 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
credential = includeDecryptedData
|
|
|
|
? // Try to get the credential with `credential:update` scope, which
|
|
|
|
// are required for decrypting the data.
|
|
|
|
await this.sharedCredentialsRepository.findCredentialForUser(
|
|
|
|
credentialId,
|
|
|
|
user,
|
|
|
|
// TODO: replace credential:update with credential:decrypt once it lands
|
|
|
|
// see: https://n8nio.slack.com/archives/C062YRE7EG4/p1708531433206069?thread_ts=1708525972.054149&cid=C062YRE7EG4
|
|
|
|
['credential:read', 'credential:update'],
|
|
|
|
)
|
|
|
|
: null;
|
2022-11-21 23:37:52 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
if (credential) {
|
|
|
|
// Decrypt the data if we found the credential with the `credential:update`
|
|
|
|
// scope.
|
2025-01-07 02:17:26 -08:00
|
|
|
decryptedData = this.credentialsService.decrypt(credential, true);
|
2024-05-17 01:53:15 -07:00
|
|
|
} else {
|
|
|
|
// Otherwise try to find them with only the `credential:read` scope. In
|
|
|
|
// that case we return them without the decrypted data.
|
|
|
|
credential = await this.sharedCredentialsRepository.findCredentialForUser(
|
|
|
|
credentialId,
|
|
|
|
user,
|
|
|
|
['credential:read'],
|
|
|
|
);
|
|
|
|
}
|
2022-11-21 23:37:52 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
if (!credential) {
|
|
|
|
throw new NotFoundError(
|
|
|
|
'Could not load the credential. If you think this is an error, ask the owner to share it with you again',
|
|
|
|
);
|
|
|
|
}
|
2024-01-31 00:48:48 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
credential = this.ownershipService.addOwnedByAndSharedWith(credential);
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const { data: _, ...rest } = credential;
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
if (decryptedData) {
|
|
|
|
return { data: decryptedData, ...rest };
|
|
|
|
}
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
return { ...rest };
|
2022-09-21 01:20:29 -07:00
|
|
|
}
|
2024-06-04 04:54:48 -07:00
|
|
|
|
|
|
|
async transferOne(user: User, credentialId: string, destinationProjectId: string) {
|
|
|
|
// 1. get credential
|
|
|
|
const credential = await this.sharedCredentialsRepository.findCredentialForUser(
|
|
|
|
credentialId,
|
|
|
|
user,
|
|
|
|
['credential:move'],
|
|
|
|
);
|
|
|
|
NotFoundError.isDefinedAndNotNull(
|
|
|
|
credential,
|
|
|
|
`Could not find the credential with the id "${credentialId}". Make sure you have the permission to move it.`,
|
|
|
|
);
|
|
|
|
|
|
|
|
// 2. get owner-sharing
|
|
|
|
const ownerSharing = credential.shared.find((s) => s.role === 'credential:owner');
|
|
|
|
NotFoundError.isDefinedAndNotNull(
|
|
|
|
ownerSharing,
|
|
|
|
`Could not find owner for credential "${credential.id}"`,
|
|
|
|
);
|
|
|
|
|
|
|
|
// 3. get source project
|
|
|
|
const sourceProject = ownerSharing.project;
|
|
|
|
|
|
|
|
// 4. get destination project
|
|
|
|
const destinationProject = await this.projectService.getProjectWithScope(
|
|
|
|
user,
|
|
|
|
destinationProjectId,
|
|
|
|
['credential:create'],
|
|
|
|
);
|
|
|
|
NotFoundError.isDefinedAndNotNull(
|
|
|
|
destinationProject,
|
|
|
|
`Could not find project with the id "${destinationProjectId}". Make sure you have the permission to create credentials in it.`,
|
|
|
|
);
|
|
|
|
|
|
|
|
// 5. checks
|
|
|
|
if (sourceProject.id === destinationProject.id) {
|
|
|
|
throw new TransferCredentialError(
|
|
|
|
"You can't transfer a credential into the project that's already owning it.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.sharedCredentialsRepository.manager.transaction(async (trx) => {
|
|
|
|
// 6. transfer the credential
|
|
|
|
// remove all sharings
|
|
|
|
await trx.remove(credential.shared);
|
|
|
|
|
|
|
|
// create new owner-sharing
|
|
|
|
await trx.save(
|
|
|
|
trx.create(SharedCredentials, {
|
|
|
|
credentialsId: credential.id,
|
|
|
|
projectId: destinationProject.id,
|
|
|
|
role: 'credential:owner',
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
2022-09-21 01:20:29 -07:00
|
|
|
}
|