n8n
Version:
n8n Workflow Automation Tool
367 lines • 18.5 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecretsProvidersConnectionsService = void 0;
const api_types_1 = require("@n8n/api-types");
const backend_common_1 = require("@n8n/backend-common");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const typeorm_1 = require("@n8n/typeorm");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const provider_registry_service_1 = require("./provider-registry.service");
const credential_dependency_service_1 = require("../../credentials/credential-dependency.service");
const bad_request_error_1 = require("../../errors/response-errors/bad-request.error");
const not_found_error_1 = require("../../errors/response-errors/not-found.error");
const event_service_1 = require("../../events/event.service");
const external_secrets_manager_ee_1 = require("../../modules/external-secrets.ee/external-secrets-manager.ee");
const redaction_service_ee_1 = require("../../modules/external-secrets.ee/redaction.service.ee");
let SecretsProvidersConnectionsService = class SecretsProvidersConnectionsService {
constructor(logger, repository, projectAccessRepository, credentialDependencyService, providerRegistry, cipher, externalSecretsManager, redactionService, eventService) {
this.logger = logger;
this.repository = repository;
this.projectAccessRepository = projectAccessRepository;
this.credentialDependencyService = credentialDependencyService;
this.providerRegistry = providerRegistry;
this.cipher = cipher;
this.externalSecretsManager = externalSecretsManager;
this.redactionService = redactionService;
this.eventService = eventService;
this.logger = this.logger.scoped('external-secrets');
}
async createConnection(proposedConnection, userId, projectRole, userRole) {
const existing = await this.repository.findOne({
where: { providerKey: proposedConnection.providerKey },
});
if (existing) {
throw new bad_request_error_1.BadRequestError(`Connection with key "${proposedConnection.providerKey}" already exists`);
}
const encryptedSettings = await this.encryptConnectionSettings(proposedConnection.settings);
const connection = this.repository.create({
...proposedConnection,
encryptedSettings,
isEnabled: true,
});
const savedConnection = await this.repository.save(connection);
if (proposedConnection.projectIds.length > 0) {
const entries = proposedConnection.projectIds.map((projectId) => this.projectAccessRepository.create({
secretsProviderConnectionId: savedConnection.id,
projectId,
role: projectRole,
}));
await this.projectAccessRepository.save(entries);
}
const result = (await this.repository.findOne({
where: { providerKey: proposedConnection.providerKey },
}));
await this.externalSecretsManager.syncProviderConnection(proposedConnection.providerKey);
this.eventService.emit('external-secrets-connection-created', {
userId,
userRole,
providerKey: result.providerKey,
vaultType: result.type,
...this.extractProjectInfo(result),
});
return result;
}
async updateProjectConnection(providerKey, updates, userId, userRole) {
const connection = await this.findConnectionOrFail(providerKey);
await this.applyConnectionUpdates(connection, updates);
await this.repository.save(connection);
return await this.syncAndEmitUpdate(providerKey, userId, userRole);
}
async updateGlobalConnection(providerKey, updates, userId, userRole) {
const connection = await this.findConnectionOrFail(providerKey);
await this.applyConnectionUpdates(connection, updates);
await this.repository.save(connection);
if (updates.projectIds !== undefined) {
const existing = await this.projectAccessRepository.findByConnectionId(connection.id);
const existingProjectIds = new Set(existing.map((e) => e.projectId));
const desiredProjectIds = new Set(updates.projectIds);
const projectIdsToRemove = existing
.filter((e) => !desiredProjectIds.has(e.projectId))
.map((e) => e.projectId);
const entriesToAdd = updates.projectIds
.filter((id) => !existingProjectIds.has(id))
.map((projectId) => ({
projectId,
role: 'secretsProviderConnection:user',
}));
await this.projectAccessRepository.updateProjectAccess(connection.id, projectIdsToRemove, entriesToAdd);
}
return await this.syncAndEmitUpdate(providerKey, userId, userRole);
}
async applyConnectionUpdates(connection, updates) {
if (updates.type !== undefined) {
connection.type = updates.type;
if (!updates.settings) {
throw new bad_request_error_1.BadRequestError('When changing the connection type, new settings must be provided as well');
}
}
if (updates.settings !== undefined) {
const savedSettings = await this.decryptConnectionSettings(connection.encryptedSettings);
const unredactedSettings = this.redactionService.unredact(updates.settings, savedSettings);
connection.encryptedSettings = await this.encryptConnectionSettings(unredactedSettings);
}
if (updates.isEnabled !== undefined) {
connection.isEnabled = updates.isEnabled;
}
}
async syncAndEmitUpdate(providerKey, userId, userRole) {
await this.externalSecretsManager.syncProviderConnection(providerKey);
const result = (await this.repository.findOne({
where: { providerKey },
}));
this.eventService.emit('external-secrets-connection-updated', {
userId,
userRole,
providerKey: result.providerKey,
vaultType: result.type,
...this.extractProjectInfo(result),
});
return result;
}
async deleteConnection(providerKey, userId, userRole) {
const connection = await this.findConnectionOrFail(providerKey);
const projectInfo = this.extractProjectInfo(connection);
const dependencyId = connection.id.toString();
await this.repository.manager.transaction(async (entityManager) => {
await this.projectAccessRepository.deleteByConnectionId(connection.id, entityManager);
await this.credentialDependencyService.deleteDependencyById({
dependencyType: credential_dependency_service_1.EXTERNAL_SECRET_PROVIDER_DEPENDENCY_TYPE,
dependencyId,
entityManager,
});
await entityManager.delete(this.repository.target, { id: connection.id });
});
await this.externalSecretsManager.syncProviderConnection(providerKey);
this.eventService.emit('external-secrets-connection-deleted', {
userId,
userRole,
providerKey: connection.providerKey,
vaultType: connection.type,
...projectInfo,
});
return connection;
}
async findConnectionOrFail(providerKey) {
const connection = await this.repository.findOne({ where: { providerKey } });
if (!connection) {
throw new not_found_error_1.NotFoundError(`Connection with key "${providerKey}" not found`);
}
return connection;
}
async getConnection(providerKey) {
const connection = await this.repository.findOne({ where: { providerKey } });
if (!connection) {
throw new not_found_error_1.NotFoundError(`Connection with key "${providerKey}" not found`);
}
return connection;
}
async listConnections() {
return await this.repository.findAll();
}
async getGlobalCompletions() {
const connectedProviderKeys = this.providerRegistry.getConnectedNames();
return await this.repository.findEnabledGlobalConnections({
providerKeys: connectedProviderKeys,
});
}
async getProjectCompletions(projectId) {
const connectedProviderKeys = this.providerRegistry.getConnectedNames();
return await this.repository.findEnabledByProjectId(projectId, {
providerKeys: connectedProviderKeys,
});
}
async listConnectionsForProject(projectId) {
return await this.repository.findAllAccessibleByProjectWithProjectAccess(projectId);
}
toSecretCompletionsResponse(connections) {
return Object.fromEntries(connections.map((connection) => [
connection.providerKey,
this.externalSecretsManager.getSecretNames(connection.providerKey),
]));
}
toPublicConnectionListItem(connection) {
const secretNames = this.externalSecretsManager.getSecretNames(connection.providerKey);
const connectionInstance = this.externalSecretsManager.getProvider(connection.providerKey);
return {
id: String(connection.id),
name: connection.providerKey,
type: connection.type,
isEnabled: connection.isEnabled,
secretsCount: secretNames.length,
state: connectionInstance?.state ?? 'initializing',
projects: connection.projectAccess.map((access) => ({
id: access.project.id,
name: access.project.name,
role: access.role,
})),
createdAt: connection.createdAt.toISOString(),
updatedAt: connection.updatedAt.toISOString(),
};
}
async toPublicConnection(connection) {
const decryptedSettings = await this.decryptConnectionSettings(connection.encryptedSettings);
const properties = this.externalSecretsManager.getProviderProperties(connection.type);
const redactedSettings = this.redactionService.redact(decryptedSettings, properties);
const secretNames = this.externalSecretsManager.getSecretNames(connection.providerKey);
const connectionInstance = this.externalSecretsManager.getProvider(connection.providerKey);
return {
id: String(connection.id),
name: connection.providerKey,
type: connection.type,
isEnabled: connection.isEnabled,
secretsCount: secretNames.length,
state: connectionInstance?.state ?? 'initializing',
secrets: secretNames.map((name) => ({ name })),
projects: connection.projectAccess.map((access) => ({
id: access.project.id,
name: access.project.name,
role: access.role,
})),
settings: redactedSettings,
createdAt: connection.createdAt.toISOString(),
updatedAt: connection.updatedAt.toISOString(),
};
}
async testConnection(providerKey, userId, userRole) {
const connection = await this.getConnection(providerKey);
const decryptedSettings = await this.decryptConnectionSettings(connection.encryptedSettings);
const result = await this.externalSecretsManager.testProviderSettings(connection.type, decryptedSettings);
const response = api_types_1.testSecretProviderConnectionResponseSchema.parse(result);
this.eventService.emit('external-secrets-connection-tested', {
userId,
userRole,
providerKey: connection.providerKey,
vaultType: connection.type,
...this.extractProjectInfo(connection),
isValid: response.success,
...(response.error && { errorMessage: response.error }),
});
return response;
}
async reloadConnectionSecrets(providerKey, userId, userRole) {
try {
const connection = await this.getConnection(providerKey);
await this.externalSecretsManager.updateProvider(providerKey);
this.eventService.emit('external-secrets-connection-reloaded', {
userId,
userRole,
providerKey: connection.providerKey,
vaultType: connection.type,
...this.extractProjectInfo(connection),
});
return api_types_1.reloadSecretProviderConnectionResponseSchema.parse({ success: true });
}
catch (error) {
if (error instanceof not_found_error_1.NotFoundError) {
throw error;
}
this.logger.warn(`Failed to reload provider ${providerKey}`, { providerKey });
return api_types_1.reloadSecretProviderConnectionResponseSchema.parse({ success: false });
}
}
extractProjectInfo(connection) {
return {
projects: connection.projectAccess.map((access) => ({
id: access.project.id,
name: access.project.name,
})),
};
}
async cleanupConnectionsForProjectDeletion(projectId) {
const accessEntries = await this.projectAccessRepository.findByProjectId(projectId);
const providerKeysToSync = new Set();
const ownerConnectionIds = new Set();
const nonOwnerConnectionIds = new Set();
for (const access of accessEntries) {
providerKeysToSync.add(access.secretsProviderConnection.providerKey);
if (access.role === 'secretsProviderConnection:owner') {
ownerConnectionIds.add(access.secretsProviderConnectionId);
}
else {
nonOwnerConnectionIds.add(access.secretsProviderConnectionId);
}
}
await this.repository.manager.transaction(async (entityManager) => {
if (ownerConnectionIds.size > 0) {
await this.credentialDependencyService.deleteDependenciesByIds({
dependencyType: credential_dependency_service_1.EXTERNAL_SECRET_PROVIDER_DEPENDENCY_TYPE,
dependencyIds: [...ownerConnectionIds].map((id) => id.toString()),
entityManager,
});
await entityManager.delete(this.repository.target, { id: (0, typeorm_1.In)([...ownerConnectionIds]) });
}
if (nonOwnerConnectionIds.size > 0) {
await entityManager.delete(this.projectAccessRepository.target, {
projectId,
secretsProviderConnectionId: (0, typeorm_1.In)([...nonOwnerConnectionIds]),
});
await entityManager.update(this.repository.target, { id: (0, typeorm_1.In)([...nonOwnerConnectionIds]) }, { isEnabled: false });
}
});
for (const providerKey of providerKeysToSync) {
await this.externalSecretsManager.syncProviderConnection(providerKey);
}
}
async encryptConnectionSettings(settings) {
return await this.cipher.encryptV2(settings);
}
async getConnectionForProject(providerKey, projectId) {
const connection = await this.repository.findByProviderKeyAndProjectId(providerKey, projectId);
if (!connection) {
throw new not_found_error_1.NotFoundError(`Connection with key "${providerKey}" not found`);
}
return connection;
}
async getConnectionAccessibleFromProject(providerKey, projectId) {
const connection = await this.repository.findAccessibleByProviderKeyAndProjectId(providerKey, projectId);
if (!connection) {
throw new not_found_error_1.NotFoundError(`Connection with key "${providerKey}" not found`);
}
return connection;
}
async deleteConnectionForProject(providerKey, projectId) {
const connection = await this.repository.findByProviderKeyAndProjectId(providerKey, projectId);
if (!connection) {
throw new not_found_error_1.NotFoundError(`Connection with key "${providerKey}" not found`);
}
const connectionId = connection.id;
await this.credentialDependencyService.deleteDependencyById({
dependencyType: credential_dependency_service_1.EXTERNAL_SECRET_PROVIDER_DEPENDENCY_TYPE,
dependencyId: connectionId.toString(),
});
await this.projectAccessRepository.deleteByConnectionId(connectionId);
await this.repository.delete({ id: connectionId });
await this.externalSecretsManager.syncProviderConnection(providerKey);
return connection;
}
async decryptConnectionSettings(encryptedSettings) {
const decrypted = await this.cipher.decryptV2(encryptedSettings);
const parsed = (0, n8n_workflow_1.jsonParse)(decrypted);
return parsed;
}
};
exports.SecretsProvidersConnectionsService = SecretsProvidersConnectionsService;
exports.SecretsProvidersConnectionsService = SecretsProvidersConnectionsService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
db_1.SecretsProviderConnectionRepository,
db_1.ProjectSecretsProviderAccessRepository,
credential_dependency_service_1.CredentialDependencyService,
provider_registry_service_1.ExternalSecretsProviderRegistry,
n8n_core_1.Cipher,
external_secrets_manager_ee_1.ExternalSecretsManager,
redaction_service_ee_1.RedactionService,
event_service_1.EventService])
], SecretsProvidersConnectionsService);
//# sourceMappingURL=secrets-providers-connections.service.ee.js.map