mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
chore(core): Expose owner information for environment changes (#16466)
Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
This commit is contained in:
@@ -19,6 +19,12 @@ export const SOURCE_CONTROL_FILE_STATUS = FileStatusSchema.Values;
|
|||||||
const FileLocationSchema = z.enum(['local', 'remote']);
|
const FileLocationSchema = z.enum(['local', 'remote']);
|
||||||
export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values;
|
export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values;
|
||||||
|
|
||||||
|
const ResourceOwnerSchema = z.object({
|
||||||
|
type: z.enum(['personal', 'team']),
|
||||||
|
projectId: z.string(),
|
||||||
|
projectName: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const SourceControlledFileSchema = z.object({
|
export const SourceControlledFileSchema = z.object({
|
||||||
file: z.string(),
|
file: z.string(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -29,6 +35,7 @@ export const SourceControlledFileSchema = z.object({
|
|||||||
conflict: z.boolean(),
|
conflict: z.boolean(),
|
||||||
updatedAt: z.string(),
|
updatedAt: z.string(),
|
||||||
pushed: z.boolean().optional(),
|
pushed: z.boolean().optional(),
|
||||||
|
owner: ResourceOwnerSchema.optional(), // Resource owner can be a personal email or team information
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SourceControlledFile = z.infer<typeof SourceControlledFileSchema>;
|
export type SourceControlledFile = z.infer<typeof SourceControlledFileSchema>;
|
||||||
|
|||||||
@@ -66,9 +66,14 @@ describe('SourceControlImportService', () => {
|
|||||||
id: 'workflow1',
|
id: 'workflow1',
|
||||||
versionId: 'v1',
|
versionId: 'v1',
|
||||||
name: 'Test Workflow',
|
name: 'Test Workflow',
|
||||||
|
owner: {
|
||||||
|
type: 'personal',
|
||||||
|
personalEmail: 'email@email.com',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
fsReadFile.mockResolvedValue(JSON.stringify(mockWorkflowData));
|
fsReadFile.mockResolvedValue(JSON.stringify(mockWorkflowData));
|
||||||
|
sourceControlScopedService.getAdminProjectsFromContext.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
const result = await service.getRemoteVersionIdsFromFiles(globalAdminContext);
|
const result = await service.getRemoteVersionIdsFromFiles(globalAdminContext);
|
||||||
expect(fsReadFile).toHaveBeenCalledWith(mockWorkflowFile, { encoding: 'utf8' });
|
expect(fsReadFile).toHaveBeenCalledWith(mockWorkflowFile, { encoding: 'utf8' });
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { SourceControlService } from '@/environments.ee/source-control/source-co
|
|||||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||||
|
|
||||||
import type { SourceControlImportService } from '../source-control-import.service.ee';
|
import type { SourceControlImportService } from '../source-control-import.service.ee';
|
||||||
import type { ExportableCredential } from '../types/exportable-credential';
|
import type { StatusExportableCredential } from '../types/exportable-credential';
|
||||||
import type { SourceControlWorkflowVersionId } from '../types/source-control-workflow-version-id';
|
import type { SourceControlWorkflowVersionId } from '../types/source-control-workflow-version-id';
|
||||||
|
|
||||||
describe('SourceControlService', () => {
|
describe('SourceControlService', () => {
|
||||||
@@ -157,7 +157,7 @@ describe('SourceControlService', () => {
|
|||||||
// Pushing this is conflict free.
|
// Pushing this is conflict free.
|
||||||
sourceControlImportService.getRemoteCredentialsFromFiles.mockResolvedValue([]);
|
sourceControlImportService.getRemoteCredentialsFromFiles.mockResolvedValue([]);
|
||||||
sourceControlImportService.getLocalCredentialsFromDb.mockResolvedValue([
|
sourceControlImportService.getLocalCredentialsFromDb.mockResolvedValue([
|
||||||
mock<ExportableCredential & { filename: string }>(),
|
mock<StatusExportableCredential>(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Define a variable that does only exist locally.
|
// Define a variable that does only exist locally.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import { SourceControlScopedService } from './source-control-scoped.service';
|
|||||||
import type { ExportResult } from './types/export-result';
|
import type { ExportResult } from './types/export-result';
|
||||||
import type { ExportableCredential } from './types/exportable-credential';
|
import type { ExportableCredential } from './types/exportable-credential';
|
||||||
import type { ExportableWorkflow } from './types/exportable-workflow';
|
import type { ExportableWorkflow } from './types/exportable-workflow';
|
||||||
import type { ResourceOwner } from './types/resource-owner';
|
import type { RemoteResourceOwner } from './types/resource-owner';
|
||||||
import type { SourceControlContext } from './types/source-control-context';
|
import type { SourceControlContext } from './types/source-control-context';
|
||||||
import { VariablesService } from '../variables/variables.service.ee';
|
import { VariablesService } from '../variables/variables.service.ee';
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export class SourceControlExportService {
|
|||||||
|
|
||||||
private async writeExportableWorkflowsToExportFolder(
|
private async writeExportableWorkflowsToExportFolder(
|
||||||
workflowsToBeExported: IWorkflowDb[],
|
workflowsToBeExported: IWorkflowDb[],
|
||||||
owners: Record<string, ResourceOwner>,
|
owners: Record<string, RemoteResourceOwner>,
|
||||||
) {
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
workflowsToBeExported.map(async (e) => {
|
workflowsToBeExported.map(async (e) => {
|
||||||
@@ -133,7 +133,7 @@ export class SourceControlExportService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// determine owner of each workflow to be exported
|
// determine owner of each workflow to be exported
|
||||||
const owners: Record<string, ResourceOwner> = {};
|
const owners: Record<string, RemoteResourceOwner> = {};
|
||||||
sharedWorkflows.forEach((sharedWorkflow) => {
|
sharedWorkflows.forEach((sharedWorkflow) => {
|
||||||
const project = sharedWorkflow.project;
|
const project = sharedWorkflow.project;
|
||||||
|
|
||||||
@@ -154,6 +154,8 @@ export class SourceControlExportService {
|
|||||||
}
|
}
|
||||||
owners[sharedWorkflow.workflowId] = {
|
owners[sharedWorkflow.workflowId] = {
|
||||||
type: 'personal',
|
type: 'personal',
|
||||||
|
projectId: project.id,
|
||||||
|
projectName: project.name,
|
||||||
personalEmail: ownerRelation.user.email,
|
personalEmail: ownerRelation.user.email,
|
||||||
};
|
};
|
||||||
} else if (project.type === 'team') {
|
} else if (project.type === 'team') {
|
||||||
@@ -256,12 +258,11 @@ export class SourceControlExportService {
|
|||||||
|
|
||||||
// keep all folders that are not accessible by the current user
|
// keep all folders that are not accessible by the current user
|
||||||
// if allowedProjects is undefined, all folders are accessible by the current user
|
// if allowedProjects is undefined, all folders are accessible by the current user
|
||||||
const foldersToKeepUnchanged =
|
const foldersToKeepUnchanged = context.hasAccessToAllProjects()
|
||||||
allowedProjects === undefined
|
? existingFolders.folders
|
||||||
? []
|
: existingFolders.folders.filter((folder) => {
|
||||||
: existingFolders.folders.filter((folder) => {
|
return !allowedProjects.some((project) => project.id === folder.homeProjectId);
|
||||||
return !allowedProjects.some((project) => project.id === folder.homeProjectId);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const newFolders = foldersToKeepUnchanged.concat(
|
const newFolders = foldersToKeepUnchanged.concat(
|
||||||
...folders.map((f) => ({
|
...folders.map((f) => ({
|
||||||
@@ -402,7 +403,7 @@ export class SourceControlExportService {
|
|||||||
const { name, type, data, id } = sharing.credentials;
|
const { name, type, data, id } = sharing.credentials;
|
||||||
const credentials = new Credentials({ id, name }, type, data);
|
const credentials = new Credentials({ id, name }, type, data);
|
||||||
|
|
||||||
let owner: ResourceOwner | null = null;
|
let owner: RemoteResourceOwner | null = null;
|
||||||
if (sharing.project.type === 'personal') {
|
if (sharing.project.type === 'personal') {
|
||||||
const ownerRelation = sharing.project.projectRelations.find(
|
const ownerRelation = sharing.project.projectRelations.find(
|
||||||
(pr) => pr.role === 'project:personalOwner',
|
(pr) => pr.role === 'project:personalOwner',
|
||||||
@@ -410,6 +411,8 @@ export class SourceControlExportService {
|
|||||||
if (ownerRelation) {
|
if (ownerRelation) {
|
||||||
owner = {
|
owner = {
|
||||||
type: 'personal',
|
type: 'personal',
|
||||||
|
projectId: sharing.project.id,
|
||||||
|
projectName: sharing.project.name,
|
||||||
personalEmail: ownerRelation.user.email,
|
personalEmail: ownerRelation.user.email,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,14 +41,67 @@ import {
|
|||||||
} from './constants';
|
} from './constants';
|
||||||
import { getCredentialExportPath, getWorkflowExportPath } from './source-control-helper.ee';
|
import { getCredentialExportPath, getWorkflowExportPath } from './source-control-helper.ee';
|
||||||
import { SourceControlScopedService } from './source-control-scoped.service';
|
import { SourceControlScopedService } from './source-control-scoped.service';
|
||||||
import type { ExportableCredential } from './types/exportable-credential';
|
import type {
|
||||||
|
ExportableCredential,
|
||||||
|
StatusExportableCredential,
|
||||||
|
} from './types/exportable-credential';
|
||||||
import type { ExportableFolder } from './types/exportable-folders';
|
import type { ExportableFolder } from './types/exportable-folders';
|
||||||
import type { ExportableTags } from './types/exportable-tags';
|
import type { ExportableTags } from './types/exportable-tags';
|
||||||
import type { ResourceOwner } from './types/resource-owner';
|
import type { StatusResourceOwner, RemoteResourceOwner } from './types/resource-owner';
|
||||||
import type { SourceControlContext } from './types/source-control-context';
|
import type { SourceControlContext } from './types/source-control-context';
|
||||||
import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id';
|
import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id';
|
||||||
import { VariablesService } from '../variables/variables.service.ee';
|
import { VariablesService } from '../variables/variables.service.ee';
|
||||||
|
|
||||||
|
const findOwnerProject = (
|
||||||
|
owner: RemoteResourceOwner,
|
||||||
|
accessibleProjects: Project[],
|
||||||
|
): Project | undefined => {
|
||||||
|
if (typeof owner === 'string') {
|
||||||
|
return accessibleProjects.find((project) =>
|
||||||
|
project.projectRelations.some(
|
||||||
|
(r) => r.role === 'project:personalOwner' && r.user.email === owner,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (owner.type === 'personal') {
|
||||||
|
return accessibleProjects.find(
|
||||||
|
(project) =>
|
||||||
|
project.type === 'personal' &&
|
||||||
|
project.projectRelations.some(
|
||||||
|
(r) => r.role === 'project:personalOwner' && r.user.email === owner.personalEmail,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return accessibleProjects.find(
|
||||||
|
(project) => project.type === 'team' && project.id === owner.teamId,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOwnerFromProject = (remoteOwnerProject: Project): StatusResourceOwner | undefined => {
|
||||||
|
let owner: StatusResourceOwner | undefined = undefined;
|
||||||
|
|
||||||
|
if (remoteOwnerProject?.type === 'personal') {
|
||||||
|
const personalEmail = remoteOwnerProject.projectRelations?.find(
|
||||||
|
(r) => r.role === 'project:personalOwner',
|
||||||
|
)?.user?.email;
|
||||||
|
|
||||||
|
if (personalEmail) {
|
||||||
|
owner = {
|
||||||
|
type: 'personal',
|
||||||
|
projectId: remoteOwnerProject.id,
|
||||||
|
projectName: remoteOwnerProject.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (remoteOwnerProject?.type === 'team') {
|
||||||
|
owner = {
|
||||||
|
type: 'team',
|
||||||
|
projectId: remoteOwnerProject.id,
|
||||||
|
projectName: remoteOwnerProject.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return owner;
|
||||||
|
};
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SourceControlImportService {
|
export class SourceControlImportService {
|
||||||
private gitFolder: string;
|
private gitFolder: string;
|
||||||
@@ -109,26 +162,21 @@ export class SourceControlImportService {
|
|||||||
if (!remote?.id) {
|
if (!remote?.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Array.isArray(accessibleProjects)) {
|
return (
|
||||||
const owner = remote.owner;
|
context.hasAccessToAllProjects() || findOwnerProject(remote.owner, accessibleProjects)
|
||||||
// The workflow `remote` belongs not to a project, that the context has access to
|
);
|
||||||
return (
|
|
||||||
typeof owner === 'object' &&
|
|
||||||
owner?.type === 'team' &&
|
|
||||||
accessibleProjects.some((project) => project.id === owner.teamId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
})
|
||||||
.map((remote) => {
|
.map((remote) => {
|
||||||
|
const project = findOwnerProject(remote.owner, accessibleProjects);
|
||||||
return {
|
return {
|
||||||
id: remote.id,
|
id: remote.id,
|
||||||
versionId: remote.versionId,
|
versionId: remote.versionId ?? '',
|
||||||
name: remote.name,
|
name: remote.name,
|
||||||
parentFolderId: remote.parentFolderId,
|
parentFolderId: remote.parentFolderId,
|
||||||
remoteId: remote.id,
|
remoteId: remote.id,
|
||||||
filename: getWorkflowExportPath(remote.id, this.workflowExportFolder),
|
filename: getWorkflowExportPath(remote.id, this.workflowExportFolder),
|
||||||
} as SourceControlWorkflowVersionId;
|
owner: project ? getOwnerFromProject(project) : undefined,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return remoteWorkflowFilesParsed;
|
return remoteWorkflowFilesParsed;
|
||||||
@@ -178,6 +226,13 @@ export class SourceControlImportService {
|
|||||||
const localWorkflows = await this.workflowRepository.find({
|
const localWorkflows = await this.workflowRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
parentFolder: true,
|
parentFolder: true,
|
||||||
|
shared: {
|
||||||
|
project: {
|
||||||
|
projectRelations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -187,9 +242,24 @@ export class SourceControlImportService {
|
|||||||
parentFolder: {
|
parentFolder: {
|
||||||
id: true,
|
id: true,
|
||||||
},
|
},
|
||||||
|
shared: {
|
||||||
|
project: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
projectRelations: {
|
||||||
|
role: true,
|
||||||
|
user: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
where: this.sourceControlScopedService.getWorkflowsInAdminProjectsFromContextFilter(context),
|
where: this.sourceControlScopedService.getWorkflowsInAdminProjectsFromContextFilter(context),
|
||||||
});
|
});
|
||||||
|
|
||||||
return localWorkflows.map((local) => {
|
return localWorkflows.map((local) => {
|
||||||
let updatedAt: Date;
|
let updatedAt: Date;
|
||||||
if (local.updatedAt instanceof Date) {
|
if (local.updatedAt instanceof Date) {
|
||||||
@@ -203,6 +273,8 @@ export class SourceControlImportService {
|
|||||||
});
|
});
|
||||||
updatedAt = isNaN(Date.parse(local.updatedAt)) ? new Date() : new Date(local.updatedAt);
|
updatedAt = isNaN(Date.parse(local.updatedAt)) ? new Date() : new Date(local.updatedAt);
|
||||||
}
|
}
|
||||||
|
const remoteOwnerProject = local.shared?.find((s) => s.role === 'workflow:owner')?.project;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: local.id,
|
id: local.id,
|
||||||
versionId: local.versionId,
|
versionId: local.versionId,
|
||||||
@@ -211,13 +283,14 @@ export class SourceControlImportService {
|
|||||||
parentFolderId: local.parentFolder?.id ?? null,
|
parentFolderId: local.parentFolder?.id ?? null,
|
||||||
filename: getWorkflowExportPath(local.id, this.workflowExportFolder),
|
filename: getWorkflowExportPath(local.id, this.workflowExportFolder),
|
||||||
updatedAt: updatedAt.toISOString(),
|
updatedAt: updatedAt.toISOString(),
|
||||||
|
owner: remoteOwnerProject ? getOwnerFromProject(remoteOwnerProject) : undefined,
|
||||||
};
|
};
|
||||||
}) as SourceControlWorkflowVersionId[];
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRemoteCredentialsFromFiles(
|
async getRemoteCredentialsFromFiles(
|
||||||
context: SourceControlContext,
|
context: SourceControlContext,
|
||||||
): Promise<Array<ExportableCredential & { filename: string }>> {
|
): Promise<StatusExportableCredential[]> {
|
||||||
const remoteCredentialFiles = await glob('*.json', {
|
const remoteCredentialFiles = await glob('*.json', {
|
||||||
cwd: this.credentialExportFolder,
|
cwd: this.credentialExportFolder,
|
||||||
absolute: true,
|
absolute: true,
|
||||||
@@ -241,43 +314,79 @@ export class SourceControlImportService {
|
|||||||
if (!remote?.id) {
|
if (!remote?.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Array.isArray(accessibleProjects)) {
|
const owner = remote.ownedBy;
|
||||||
const owner = remote.ownedBy;
|
// The credential `remote` belongs not to a project, that the context has access to
|
||||||
// The credential `remote` belongs not to a project, that the context has access to
|
return (
|
||||||
return (
|
!owner || context.hasAccessToAllProjects() || findOwnerProject(owner, accessibleProjects)
|
||||||
typeof owner === 'object' &&
|
);
|
||||||
owner?.type === 'team' &&
|
|
||||||
accessibleProjects.some((project) => project.id === owner.teamId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
})
|
||||||
.map((remote) => {
|
.map((remote) => {
|
||||||
|
const project = remote.ownedBy
|
||||||
|
? findOwnerProject(remote.ownedBy, accessibleProjects)
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
...remote,
|
...remote,
|
||||||
|
ownedBy: project
|
||||||
|
? {
|
||||||
|
type: project.type,
|
||||||
|
projectId: project.id,
|
||||||
|
projectName: project.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
filename: getCredentialExportPath(remote.id, this.credentialExportFolder),
|
filename: getCredentialExportPath(remote.id, this.credentialExportFolder),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return remoteCredentialFilesParsed.filter((e) => e !== undefined) as Array<
|
return remoteCredentialFilesParsed.filter(
|
||||||
ExportableCredential & { filename: string }
|
(e) => e !== undefined,
|
||||||
>;
|
) as StatusExportableCredential[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLocalCredentialsFromDb(
|
async getLocalCredentialsFromDb(
|
||||||
context: SourceControlContext,
|
context: SourceControlContext,
|
||||||
): Promise<Array<ExportableCredential & { filename: string }>> {
|
): Promise<StatusExportableCredential[]> {
|
||||||
const localCredentials = await this.credentialsRepository.find({
|
const localCredentials = await this.credentialsRepository.find({
|
||||||
select: ['id', 'name', 'type'],
|
relations: {
|
||||||
|
shared: {
|
||||||
|
project: {
|
||||||
|
projectRelations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
shared: {
|
||||||
|
project: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
projectRelations: {
|
||||||
|
role: true,
|
||||||
|
user: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
where:
|
where:
|
||||||
this.sourceControlScopedService.getCredentialsInAdminProjectsFromContextFilter(context),
|
this.sourceControlScopedService.getCredentialsInAdminProjectsFromContextFilter(context),
|
||||||
});
|
});
|
||||||
return localCredentials.map((local) => ({
|
return localCredentials.map((local) => {
|
||||||
id: local.id,
|
const remoteOwnerProject = local.shared?.find((s) => s.role === 'credential:owner')?.project;
|
||||||
name: local.name,
|
return {
|
||||||
type: local.type,
|
id: local.id,
|
||||||
filename: getCredentialExportPath(local.id, this.credentialExportFolder),
|
name: local.name,
|
||||||
})) as Array<ExportableCredential & { filename: string }>;
|
type: local.type,
|
||||||
|
filename: getCredentialExportPath(local.id, this.credentialExportFolder),
|
||||||
|
ownedBy: remoteOwnerProject ? getOwnerFromProject(remoteOwnerProject) : undefined,
|
||||||
|
};
|
||||||
|
}) as StatusExportableCredential[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRemoteVariablesFromFile(): Promise<Variables[]> {
|
async getRemoteVariablesFromFile(): Promise<Variables[]> {
|
||||||
@@ -316,11 +425,11 @@ export class SourceControlImportService {
|
|||||||
const accessibleProjects =
|
const accessibleProjects =
|
||||||
await this.sourceControlScopedService.getAdminProjectsFromContext(context);
|
await this.sourceControlScopedService.getAdminProjectsFromContext(context);
|
||||||
|
|
||||||
if (Array.isArray(accessibleProjects)) {
|
mappedFolders.folders = mappedFolders.folders.filter(
|
||||||
mappedFolders.folders = mappedFolders.folders.filter((folder) =>
|
(folder) =>
|
||||||
|
context.hasAccessToAllProjects() ||
|
||||||
accessibleProjects.some((project) => project.id === folder.homeProjectId),
|
accessibleProjects.some((project) => project.id === folder.homeProjectId),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return mappedFolders;
|
return mappedFolders;
|
||||||
}
|
}
|
||||||
@@ -796,7 +905,7 @@ export class SourceControlImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findOrCreateOwnerProject(owner: ResourceOwner): Promise<Project | null> {
|
private async findOrCreateOwnerProject(owner: RemoteResourceOwner): Promise<Project | null> {
|
||||||
if (typeof owner === 'string' || owner.type === 'personal') {
|
if (typeof owner === 'string' || owner.type === 'personal') {
|
||||||
const email = typeof owner === 'string' ? owner : owner.personalEmail;
|
const email = typeof owner === 'string' ? owner : owner.personalEmail;
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
@@ -834,7 +943,7 @@ export class SourceControlImportService {
|
|||||||
|
|
||||||
assertNever(owner);
|
assertNever(owner);
|
||||||
|
|
||||||
const errorOwner = owner as ResourceOwner;
|
const errorOwner = owner as RemoteResourceOwner;
|
||||||
throw new UnexpectedError(
|
throw new UnexpectedError(
|
||||||
`Unknown resource owner type "${
|
`Unknown resource owner type "${
|
||||||
typeof errorOwner !== 'string' ? errorOwner.type : 'UNKNOWN'
|
typeof errorOwner !== 'string' ? errorOwner.type : 'UNKNOWN'
|
||||||
|
|||||||
@@ -37,19 +37,28 @@ export class SourceControlScopedService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAdminProjectsFromContext(context: SourceControlContext): Promise<Project[] | undefined> {
|
async getAdminProjectsFromContext(context: SourceControlContext): Promise<Project[]> {
|
||||||
if (context.hasAccessToAllProjects()) {
|
if (context.hasAccessToAllProjects()) {
|
||||||
// In case the user is a global admin or owner, we don't need a filter
|
// In case the user is a global admin or owner, we don't need a filter
|
||||||
return;
|
return await this.projectRepository.find({
|
||||||
|
relations: {
|
||||||
|
projectRelations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.projectRepository.find({
|
return await this.projectRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
projectRelations: true,
|
projectRelations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
type: true,
|
||||||
},
|
},
|
||||||
where: this.getAdminProjectsByContextFilter(context),
|
where: this.getAdminProjectsByContextFilter(context),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
} from './source-control-helper.ee';
|
} from './source-control-helper.ee';
|
||||||
import { SourceControlImportService } from './source-control-import.service.ee';
|
import { SourceControlImportService } from './source-control-import.service.ee';
|
||||||
import { SourceControlPreferencesService } from './source-control-preferences.service.ee';
|
import { SourceControlPreferencesService } from './source-control-preferences.service.ee';
|
||||||
import type { ExportableCredential } from './types/exportable-credential';
|
import type { StatusExportableCredential } from './types/exportable-credential';
|
||||||
import type { ExportableFolder } from './types/exportable-folders';
|
import type { ExportableFolder } from './types/exportable-folders';
|
||||||
import type { ImportResult } from './types/import-result';
|
import type { ImportResult } from './types/import-result';
|
||||||
import { SourceControlContext } from './types/source-control-context';
|
import { SourceControlContext } from './types/source-control-context';
|
||||||
@@ -663,6 +663,7 @@ export class SourceControlService {
|
|||||||
conflict: false,
|
conflict: false,
|
||||||
file: item.filename,
|
file: item.filename,
|
||||||
updatedAt: item.updatedAt ?? new Date().toISOString(),
|
updatedAt: item.updatedAt ?? new Date().toISOString(),
|
||||||
|
owner: item.owner,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -676,6 +677,7 @@ export class SourceControlService {
|
|||||||
conflict: options.direction === 'push' ? false : true,
|
conflict: options.direction === 'push' ? false : true,
|
||||||
file: item.filename,
|
file: item.filename,
|
||||||
updatedAt: item.updatedAt ?? new Date().toISOString(),
|
updatedAt: item.updatedAt ?? new Date().toISOString(),
|
||||||
|
owner: item.owner,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -689,6 +691,7 @@ export class SourceControlService {
|
|||||||
conflict: true,
|
conflict: true,
|
||||||
file: item.filename,
|
file: item.filename,
|
||||||
updatedAt: item.updatedAt ?? new Date().toISOString(),
|
updatedAt: item.updatedAt ?? new Date().toISOString(),
|
||||||
|
owner: item.owner,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -719,11 +722,7 @@ export class SourceControlService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// only compares the name, since that is the only change synced for credentials
|
// only compares the name, since that is the only change synced for credentials
|
||||||
const credModifiedInEither: Array<
|
const credModifiedInEither: StatusExportableCredential[] = [];
|
||||||
ExportableCredential & {
|
|
||||||
filename: string;
|
|
||||||
}
|
|
||||||
> = [];
|
|
||||||
credLocalIds.forEach((local) => {
|
credLocalIds.forEach((local) => {
|
||||||
const mismatchingCreds = credRemoteIds.find((remote) => {
|
const mismatchingCreds = credRemoteIds.find((remote) => {
|
||||||
return remote.id === local.id && (remote.name !== local.name || remote.type !== local.type);
|
return remote.id === local.id && (remote.name !== local.name || remote.type !== local.type);
|
||||||
@@ -746,6 +745,7 @@ export class SourceControlService {
|
|||||||
conflict: false,
|
conflict: false,
|
||||||
file: item.filename,
|
file: item.filename,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
owner: item.ownedBy,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -759,6 +759,7 @@ export class SourceControlService {
|
|||||||
conflict: options.direction === 'push' ? false : true,
|
conflict: options.direction === 'push' ? false : true,
|
||||||
file: item.filename,
|
file: item.filename,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
owner: item.ownedBy,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -772,6 +773,7 @@ export class SourceControlService {
|
|||||||
conflict: true,
|
conflict: true,
|
||||||
file: item.filename,
|
file: item.filename,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
owner: item.ownedBy,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { ResourceOwner } from './resource-owner';
|
import type { RemoteResourceOwner, StatusResourceOwner } from './resource-owner';
|
||||||
|
|
||||||
export interface ExportableCredential {
|
export interface ExportableCredential {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,5 +12,10 @@ export interface ExportableCredential {
|
|||||||
* Email of the user who owns this credential at the source instance.
|
* Email of the user who owns this credential at the source instance.
|
||||||
* Ownership is mirrored at target instance if user is also present there.
|
* Ownership is mirrored at target instance if user is also present there.
|
||||||
*/
|
*/
|
||||||
ownedBy: ResourceOwner | null;
|
ownedBy: RemoteResourceOwner | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StatusExportableCredential = ExportableCredential & {
|
||||||
|
filename: string;
|
||||||
|
ownedBy?: StatusResourceOwner;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { INode, IConnections, IWorkflowSettings } from 'n8n-workflow';
|
import type { INode, IConnections, IWorkflowSettings } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { ResourceOwner } from './resource-owner';
|
import type { RemoteResourceOwner } from './resource-owner';
|
||||||
|
|
||||||
export interface ExportableWorkflow {
|
export interface ExportableWorkflow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -10,7 +10,7 @@ export interface ExportableWorkflow {
|
|||||||
settings?: IWorkflowSettings;
|
settings?: IWorkflowSettings;
|
||||||
triggerCount: number;
|
triggerCount: number;
|
||||||
versionId?: string;
|
versionId?: string;
|
||||||
owner: ResourceOwner;
|
owner: RemoteResourceOwner;
|
||||||
parentFolderId: string | null;
|
parentFolderId: string | null;
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export type ResourceOwner =
|
export type RemoteResourceOwner =
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
type: 'personal';
|
type: 'personal';
|
||||||
|
projectId?: string; // Optional for retrocompatibility
|
||||||
|
projectName?: string; // Optional for retrocompatibility
|
||||||
personalEmail: string;
|
personalEmail: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -9,3 +11,9 @@ export type ResourceOwner =
|
|||||||
teamId: string;
|
teamId: string;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StatusResourceOwner = {
|
||||||
|
type: 'personal' | 'team';
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { StatusResourceOwner } from './resource-owner';
|
||||||
|
|
||||||
export interface SourceControlWorkflowVersionId {
|
export interface SourceControlWorkflowVersionId {
|
||||||
id: string;
|
id: string;
|
||||||
versionId: string;
|
versionId: string;
|
||||||
@@ -7,4 +9,5 @@ export interface SourceControlWorkflowVersionId {
|
|||||||
remoteId?: string;
|
remoteId?: string;
|
||||||
parentFolderId: string | null;
|
parentFolderId: string | null;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
|
owner?: StatusResourceOwner;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { SourceControlService } from '@/environments.ee/source-control/source-co
|
|||||||
import type { ExportableCredential } from '@/environments.ee/source-control/types/exportable-credential';
|
import type { ExportableCredential } from '@/environments.ee/source-control/types/exportable-credential';
|
||||||
import type { ExportableFolder } from '@/environments.ee/source-control/types/exportable-folders';
|
import type { ExportableFolder } from '@/environments.ee/source-control/types/exportable-folders';
|
||||||
import type { ExportableWorkflow } from '@/environments.ee/source-control/types/exportable-workflow';
|
import type { ExportableWorkflow } from '@/environments.ee/source-control/types/exportable-workflow';
|
||||||
import type { ResourceOwner } from '@/environments.ee/source-control/types/resource-owner';
|
import type { RemoteResourceOwner } from '@/environments.ee/source-control/types/resource-owner';
|
||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||||
import { EventService } from '@/events/event.service';
|
import { EventService } from '@/events/event.service';
|
||||||
@@ -68,7 +68,7 @@ function toExportableCredential(
|
|||||||
cred: CredentialsEntity,
|
cred: CredentialsEntity,
|
||||||
owner: Project | User,
|
owner: Project | User,
|
||||||
): ExportableCredential {
|
): ExportableCredential {
|
||||||
let resourceOwner: ResourceOwner;
|
let resourceOwner: RemoteResourceOwner;
|
||||||
|
|
||||||
if (owner instanceof Project) {
|
if (owner instanceof Project) {
|
||||||
resourceOwner = {
|
resourceOwner = {
|
||||||
@@ -97,7 +97,7 @@ function toExportableWorkflow(
|
|||||||
owner: Project | User,
|
owner: Project | User,
|
||||||
versionId?: string,
|
versionId?: string,
|
||||||
): ExportableWorkflow {
|
): ExportableWorkflow {
|
||||||
let resourceOwner: ResourceOwner;
|
let resourceOwner: RemoteResourceOwner;
|
||||||
|
|
||||||
if (owner instanceof Project) {
|
if (owner instanceof Project) {
|
||||||
resourceOwner = {
|
resourceOwner = {
|
||||||
|
|||||||
Reference in New Issue
Block a user