UNPKG

n8n

Version:

n8n Workflow Automation Tool

1,059 lines 53.1 kB
"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.CredentialsService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const permissions_1 = require("@n8n/permissions");
const typeorm_1 = require("@n8n/typeorm");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const credential_types_1 = require("../credential-types");
const credentials_helper_1 = require("../credentials-helper");
const credential_not_found_error_1 = require("../errors/credential-not-found.error");
const bad_request_error_1 = require("../errors/response-errors/bad-request.error");
const forbidden_error_1 = require("../errors/response-errors/forbidden.error");
const not_found_error_1 = require("../errors/response-errors/not-found.error");
const external_hooks_1 = require("../external-hooks");
const generic_helpers_1 = require("../generic-helpers");
const external_secrets_config_1 = require("../modules/external-secrets.ee/external-secrets.config");
const secret_provider_access_check_service_ee_1 = require("../modules/external-secrets.ee/secret-provider-access-check.service.ee");
const validate_oauth_url_1 = require("../oauth/validate-oauth-url");
const check_access_1 = require("../permissions.ee/check-access");
const shared_fields_1 = require("../modules/dynamic-credentials.ee/services/shared-fields");
const credentials_tester_service_1 = require("../services/credentials-tester.service");
const ownership_service_1 = require("../services/ownership.service");
const project_service_ee_1 = require("../services/project.service.ee");
const role_service_1 = require("../services/role.service");
const credential_connection_status_proxy_1 = require("./credential-connection-status-proxy");
const credential_dependency_service_1 = require("./credential-dependency.service");
const credentials_finder_service_1 = require("./credentials-finder.service");
const external_secrets_utils_1 = require("./external-secrets.utils");
const instance_credential_use_registry_1 = require("./instance-credential-use.registry");
const validation_1 = require("./validation");
const CUSTOM_AUTH_JSON_REDACTED_VALUE = '***';
let CredentialsService = class CredentialsService {
    constructor(credentialsRepository, credentialDependencyService, sharedCredentialsRepository, ownershipService, logger, errorReporter, credentialsTester, externalHooks, credentialTypes, projectRepository, projectService, roleService, userRepository, credentialsFinderService, credentialsHelper, externalSecretsConfig, externalSecretsProviderAccessCheckService, connectionStatusProxy, instanceCredentialAssignmentRepository, instanceCredentialUseRegistry, dbLockService) {
        this.credentialsRepository = credentialsRepository;
        this.credentialDependencyService = credentialDependencyService;
        this.sharedCredentialsRepository = sharedCredentialsRepository;
        this.ownershipService = ownershipService;
        this.logger = logger;
        this.errorReporter = errorReporter;
        this.credentialsTester = credentialsTester;
        this.externalHooks = externalHooks;
        this.credentialTypes = credentialTypes;
        this.projectRepository = projectRepository;
        this.projectService = projectService;
        this.roleService = roleService;
        this.userRepository = userRepository;
        this.credentialsFinderService = credentialsFinderService;
        this.credentialsHelper = credentialsHelper;
        this.externalSecretsConfig = externalSecretsConfig;
        this.externalSecretsProviderAccessCheckService = externalSecretsProviderAccessCheckService;
        this.connectionStatusProxy = connectionStatusProxy;
        this.instanceCredentialAssignmentRepository = instanceCredentialAssignmentRepository;
        this.instanceCredentialUseRegistry = instanceCredentialUseRegistry;
        this.dbLockService = dbLockService;
    }
    async countConnectedUsers(credentialId) {
        return await this.connectionStatusProxy.countConnectedUsers(credentialId);
    }
    async populateConnectedByMe(credentials, user) {
        const resolvable = credentials.filter((c) => c.isResolvable === true);
        if (resolvable.length === 0)
            return;
        const connected = await this.connectionStatusProxy.findConnectedCredentialIds(user.id, resolvable.map((c) => c.id));
        for (const c of resolvable) {
            c.connectedByMe = connected.has(c.id);
        }
    }
    async addGlobalCredentials(credentials, includeData, dependencyFilter, type) {
        const globalCredentials = await this.credentialsRepository.findAllGlobalCredentials({
            includeData,
            ...(type ? { type } : {}),
            filters: { dependency: dependencyFilter },
        });
        const credentialIds = new Set(credentials.map((c) => c.id));
        const newGlobalCreds = globalCredentials.filter((gc) => !credentialIds.has(gc.id));
        return [...credentials, ...newGlobalCreds];
    }
    extractTypeFilter(listQueryOptions) {
        const filterType = listQueryOptions.filter?.type;
        return typeof filterType === 'string' && filterType !== '' ? filterType : undefined;
    }
    async getMany(user, { listQueryOptions = {}, includeScopes = false, includeData = false, onlySharedWithMe = false, includeGlobal = false, filters = {}, } = {}) {
        const { externalSecretsStore } = filters;
        const returnAll = (0, permissions_1.hasGlobalScope)(user, 'credential:list');
        const isDefaultSelect = !listQueryOptions.select;
        const dependencyFilter = externalSecretsStore
            ? await this.credentialDependencyService.resolveExternalSecretsStoreDependencyFilter(externalSecretsStore)
            : undefined;
        if (externalSecretsStore && !dependencyFilter) {
            return [];
        }
        if (includeData) {
            includeScopes = true;
        }
        let credentials;
        if (returnAll) {
            credentials = await this.getManyForAdminUser(user, {
                listQueryOptions,
                includeGlobal,
                includeData,
                onlySharedWithMe,
                filters: { dependency: dependencyFilter },
            });
        }
        else {
            credentials = await this.getManyForMemberUser(user, {
                listQueryOptions,
                includeGlobal,
                includeData,
                onlySharedWithMe,
                filters: { dependency: dependencyFilter },
            });
        }
        return await this.enrichCredentials(credentials, user, isDefaultSelect, includeScopes, includeData, listQueryOptions, onlySharedWithMe);
    }
    async getManyForAdminUser(user, { listQueryOptions, includeGlobal, includeData, onlySharedWithMe, filters, }) {
        const { dependency: dependencyFilter } = filters ?? {};
        const typeFilter = this.extractTypeFilter(listQueryOptions);
        if (onlySharedWithMe || dependencyFilter) {
            const sharingOptions = {
                ...(onlySharedWithMe ? { onlySharedWithMe: true } : {}),
            };
            const { credentials } = await this.credentialsRepository.getManyAndCountWithSharingSubquery(user, sharingOptions, {
                ...listQueryOptions,
                ...(includeData ? { includeData: true } : {}),
                filters: {
                    dependency: dependencyFilter,
                },
            });
            if (includeGlobal) {
                return await this.addGlobalCredentials(credentials, includeData, dependencyFilter, typeFilter);
            }
            return credentials;
        }
        await this.applyPersonalProjectFilter(listQueryOptions);
        let credentials = await this.credentialsRepository.findMany({
            ...listQueryOptions,
            ...(includeData ? { includeData: true } : {}),
        });
        if (includeGlobal) {
            credentials = await this.addGlobalCredentials(credentials, includeData, dependencyFilter, typeFilter);
        }
        return credentials;
    }
    async getManyForMemberUser(user, { listQueryOptions, includeGlobal, includeData, onlySharedWithMe, filters, }) {
        const { dependency: dependencyFilter } = filters ?? {};
        const typeFilter = this.extractTypeFilter(listQueryOptions);
        let isPersonalProject = false;
        let personalProjectOwnerId = null;
        if (listQueryOptions.filter?.projectId) {
            const project = await this.projectRepository.findOneBy({
                id: listQueryOptions.filter.projectId,
            });
            if (!project) {
                return [];
            }
            isPersonalProject = project.type === 'personal';
            personalProjectOwnerId = project.creatorId;
        }
        const sharingOptions = {};
        if (isPersonalProject && personalProjectOwnerId) {
            if (personalProjectOwnerId !== user.id && !(0, permissions_1.hasGlobalScope)(user, 'credential:read')) {
                return [];
            }
            sharingOptions.isPersonalProject = true;
            sharingOptions.personalProjectOwnerId = personalProjectOwnerId;
        }
        else if (onlySharedWithMe) {
            sharingOptions.onlySharedWithMe = true;
        }
        else {
            const projectRoles = await this.roleService.rolesWithScope('project', ['credential:read']);
            const credentialRoles = await this.roleService.rolesWithScope('credential', [
                'credential:read',
            ]);
            sharingOptions.scopes = ['credential:read'];
            sharingOptions.projectRoles = projectRoles;
            sharingOptions.credentialRoles = credentialRoles;
        }
        const { credentials } = await this.credentialsRepository.getManyAndCountWithSharingSubquery(user, sharingOptions, {
            ...listQueryOptions,
            ...(includeData ? { includeData: true } : {}),
            filters: {
                dependency: dependencyFilter,
            },
        });
        if (includeGlobal) {
            return await this.addGlobalCredentials(credentials, includeData, dependencyFilter, typeFilter);
        }
        return credentials;
    }
    async applyPersonalProjectFilter(listQueryOptions) {
        const projectId = typeof listQueryOptions.filter?.projectId === 'string'
            ? listQueryOptions.filter.projectId
            : undefined;
        if (!projectId) {
            return;
        }
        let project;
        try {
            project = await this.projectService.getProject(projectId);
        }
        catch { }
        if (project?.type === 'personal') {
            listQueryOptions.filter = {
                ...listQueryOptions.filter,
                withRole: 'credential:owner',
            };
        }
    }
    async enrichCredentials(credentials, user, isDefaultSelect, includeScopes, includeData, listQueryOptions, onlySharedWithMe) {
        if (isDefaultSelect) {
            credentials = await this.populateSharedRelations(credentials, listQueryOptions, onlySharedWithMe);
        }
        if (includeScopes) {
            credentials = await this.addScopesToCredentials(credentials, user);
        }
        if (includeData) {
            const decrypted = await this.addDecryptedDataToCredentials(credentials);
            await this.populateConnectedByMe(decrypted, user);
            return decrypted;
        }
        await this.populateConnectedByMe(credentials, user);
        return credentials;
    }
    async populateSharedRelations(credentials, listQueryOptions, onlySharedWithMe) {
        const needsRelations = listQueryOptions.filter?.shared &&
            typeof listQueryOptions.filter.shared === 'object' &&
            'projectId' in listQueryOptions.filter.shared
            ? listQueryOptions.filter.shared.projectId
            : onlySharedWithMe;
        if (needsRelations) {
            const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(credentials.map((c) => c.id));
            credentials.forEach((c) => {
                c.shared = relations.filter((r) => r.credentialsId === c.id);
            });
        }
        return credentials.map((c) => this.ownershipService.addOwnedByAndSharedWith(c));
    }
    async addScopesToCredentials(credentials, user) {
        const projectRelations = await this.projectService.getProjectRelationsForUser(user);
        return credentials.map((c) => this.roleService.addScopes(c, user, projectRelations));
    }
    async addDecryptedDataToCredentials(credentials) {
        return await Promise.all(credentials.map(async (c) => {
            const data = c.scopes.includes('credential:update') ? await this.decrypt(c) : undefined;
            if (data?.oauthTokenData) {
                data.oauthTokenData = true;
            }
            return {
                ...c,
                data,
            };
        }));
    }
    async getCredentialsAUserCanUseInAWorkflow(user, options) {
        const projectRelations = await this.projectService.getProjectRelationsForUser(user);
        const allCredentials = await this.credentialsFinderService.findCredentialsForUser(user, [
            'credential:read',
        ]);
        const allCredentialsForWorkflow = 'workflowId' in options
            ? (await this.findAllCredentialIdsForWorkflow(options.workflowId)).map((c) => c.id)
            : (await this.findAllCredentialIdsForProject(options.projectId)).map((c) => c.id);
        const intersection = allCredentials.filter((c) => allCredentialsForWorkflow.includes(c.id) || c.isGlobal);
        if (intersection.length > 0) {
            const relations = await this.sharedCredentialsRepository.getAllRelationsForCredentials(intersection.map((c) => c.id));
            intersection.forEach((c) => {
                c.shared = relations.filter((r) => r.credentialsId === c.id);
            });
        }
        const enriched = intersection
            .map((c) => this.ownershipService.addOwnedByAndSharedWith(c))
            .map((c) => this.roleService.addScopes(c, user, projectRelations));
        const result = enriched.map((c) => ({
            id: c.id,
            name: c.name,
            type: c.type,
            createdAt: c.createdAt.toISOString(),
            updatedAt: c.updatedAt.toISOString(),
            scopes: c.scopes,
            isManaged: c.isManaged,
            isGlobal: c.isGlobal,
            isResolvable: c.isResolvable,
            homeProject: c.homeProject,
            sharedWithProjects: c.sharedWithProjects,
            currentUserHasAccess: true,
        }));
        await this.populateConnectedByMe(result, user);
        return result;
    }
    async findAllGlobalCredentialIds(includeData = false) {
        const globalCredentials = await this.credentialsRepository.findAllGlobalCredentials({
            includeData,
        });
        return globalCredentials;
    }
    async findAllCredentialIdsForWorkflow(workflowId) {
        const user = await this.userRepository.findPersonalOwnerForWorkflow(workflowId);
        if (user && (0, permissions_1.hasGlobalScope)(user, 'credential:read')) {
            return await this.credentialsRepository.findAllPersonalCredentials();
        }
        return await this.credentialsRepository.findAllCredentialsForWorkflow(workflowId);
    }
    async findAllCredentialIdsForProject(projectId) {
        const user = await this.userRepository.findPersonalOwnerForProject(projectId);
        if (user && (0, permissions_1.hasGlobalScope)(user, 'credential:read')) {
            return await this.credentialsRepository.findAllPersonalCredentials();
        }
        return await this.credentialsRepository.findAllCredentialsForProject(projectId);
    }
    async getSharing(user, credentialId, globalScopes, relations = { credentials: true }) {
        let where = {
            credentialsId: credentialId,
            credentials: { usageScope: 'project' },
        };
        if (!(0, permissions_1.hasGlobalScope)(user, globalScopes, { mode: 'allOf' })) {
            where = {
                ...where,
                role: 'credential:owner',
                project: {
                    projectRelations: {
                        role: { slug: permissions_1.PROJECT_OWNER_ROLE_SLUG },
                        userId: user.id,
                    },
                },
            };
        }
        return await this.sharedCredentialsRepository.findOne({
            where,
            relations,
        });
    }
    async prepareUpdateData(user, data, existingCredential, options) {
        const decryptedData = await this.decrypt(existingCredential, true);
        const mergedData = (0, n8n_workflow_1.deepCopy)(data);
        if (mergedData.data) {
            mergedData.data = this.unredact(mergedData.data, decryptedData, this.getCredentialTypeProperties(existingCredential.type));
        }
        if (existingCredential.usageScope === 'instance') {
            await this.validateInstanceCredentialUpdate(existingCredential, mergedData.data ?? decryptedData, undefined, options?.operationContext);
        }
        else {
            const projectOwningCredential = existingCredential.shared?.find((shared) => shared.role === 'credential:owner');
            if (!projectOwningCredential) {
                throw new not_found_error_1.NotFoundError(`Could not find owner for credential "${existingCredential.id}"`);
            }
            await (0, validation_1.validateExternalSecretsPermissions)({
                user,
                projectId: projectOwningCredential.projectId,
                dataToSave: data.data,
                decryptedExistingData: decryptedData,
            });
            if (this.externalSecretsConfig.externalSecretsForProjects && data.data) {
                await (0, validation_1.validateAccessToReferencedSecretProviders)(projectOwningCredential.projectId, data.data, this.externalSecretsProviderAccessCheckService, 'update');
            }
        }
        const updateData = this.credentialsRepository.create(mergedData);
        await (0, generic_helpers_1.validateEntity)(updateData);
        if (decryptedData.oauthTokenData && !options?.clearOauthTokenData) {
            updateData.data.oauthTokenData = decryptedData.oauthTokenData;
        }
        this.validateOAuthCredentialUrls(updateData.type, updateData.data);
        return updateData;
    }
    async createEncryptedData(credential) {
        const credentials = new n8n_core_1.Credentials({ id: credential.id, name: credential.name }, credential.type);
        await credentials.setData(credential.data);
        const newCredentialData = credentials.getDataToSave();
        newCredentialData.updatedAt = new Date();
        return newCredentialData;
    }
    async clearOauthTokenData(credential) {
        const decryptedData = await (0, credentials_helper_1.createCredentialsFromCredentialsEntity)(credential).getData();
        delete decryptedData.oauthTokenData;
        delete decryptedData.accountIdentifier;
        const newCredentialData = await this.createEncryptedData({
            id: credential.id,
            name: credential.name,
            type: credential.type,
            data: decryptedData,
        });
        await this.update(credential.id, newCredentialData, decryptedData);
    }
    async decrypt(credential, includeRawData = false) {
        const coreCredential = (0, credentials_helper_1.createCredentialsFromCredentialsEntity)(credential);
        try {
            const data = await coreCredential.getData();
            if (includeRawData) {
                return data;
            }
            return this.redact(data, credential);
        }
        catch (error) {
            if (error instanceof n8n_core_1.CredentialDataError) {
                this.errorReporter.error(error, {
                    level: 'error',
                    extra: { credentialId: credential.id },
                    tags: { credentialType: credential.type },
                });
                return {};
            }
            throw error;
        }
    }
    async update(credentialId, newCredentialData, decryptedCredentialData, options) {
        await this.externalHooks.run('credentials.update', [newCredentialData]);
        const persist = async (transactionManager) => {
            await transactionManager.update(db_1.CredentialsEntity, credentialId, newCredentialData);
            if (options?.deleteUserEntries) {
                await this.connectionStatusProxy.deleteAllUserEntries(credentialId, transactionManager);
            }
            if (decryptedCredentialData) {
                await this.credentialDependencyService.syncExternalSecretProviderDependenciesForCredential({
                    credentialId,
                    decryptedCredentialData,
                    entityManager: transactionManager,
                });
            }
            return await transactionManager.findOneBy(db_1.CredentialsEntity, { id: credentialId });
        };
        if (!options?.instanceCredential) {
            return await this.credentialsRepository.manager.transaction(persist);
        }
        const instanceCredential = options.instanceCredential;
        const hookedData = await this.getValidatedInstanceCredentialHookData(newCredentialData, instanceCredential.id, instanceCredential.type);
        return await this.dbLockService.withLock(1007, async (transactionManager, ctx) => {
            await this.validateInstanceCredentialUpdate(instanceCredential, hookedData, undefined, ctx);
            return await persist(transactionManager);
        });
    }
    async runInstanceCredentialHooks(event, credential) {
        const encrypted = await this.createEncryptedData(credential);
        await this.externalHooks.run(`credentials.${event}`, [encrypted]);
        return encrypted;
    }
    async updateInstanceCredential(user, credentialId, data, ctx, options = {}) {
        if (!(0, permissions_1.hasGlobalScope)(user, 'credential:manageInstance')) {
            throw new forbidden_error_1.ForbiddenError('You do not have permission to update provider connections');
        }
        const credential = await this.credentialsRepository.findInstanceCredentialById(credentialId, ctx);
        if (!credential) {
            throw new not_found_error_1.NotFoundError(`Credential with ID "${credentialId}" could not be found.`);
        }
        const prepared = await this.prepareUpdateData(user, data, credential, {
            operationContext: ctx,
        });
        if (prepared.type !== credential.type) {
            throw new bad_request_error_1.BadRequestError('Provider connection type cannot be changed. Create a new connection instead.');
        }
        const decryptedData = prepared.data;
        const encrypted = options.encryptedData ??
            (await this.createEncryptedData({
                id: credential.id,
                name: prepared.name,
                type: prepared.type,
                data: decryptedData,
            }));
        if (!options.skipExternalHooks) {
            await this.externalHooks.run('credentials.update', [encrypted]);
        }
        const hookedData = await this.getValidatedInstanceCredentialHookData(encrypted, credential.id, credential.type);
        await this.validateInstanceCredentialUpdate(credential, hookedData, undefined, ctx);
        const updated = await this.credentialsRepository.updateInstanceCredential(credential.id, encrypted, ctx);
        if (!updated) {
            throw new not_found_error_1.NotFoundError(`Credential with ID "${credentialId}" could not be found.`);
        }
        return updated;
    }
    async resolveOwningProjectIdForNewCredential(user, projectId, entityManager) {
        if (projectId !== undefined) {
            return projectId;
        }
        const personalProject = await this.projectRepository.getPersonalProjectForUserOrFail(user.id, entityManager);
        return personalProject.id;
    }
    async save(credential, encryptedData, user, projectId, decryptedCredentialData) {
        const newCredential = new db_1.CredentialsEntity();
        Object.assign(newCredential, credential, encryptedData);
        await this.externalHooks.run('credentials.create', [encryptedData]);
        const { manager: dbManager } = this.credentialsRepository;
        const result = await dbManager.transaction(async (transactionManager) => {
            const project = await this.projectService.getProjectWithScope(user, projectId, ['credential:create'], transactionManager);
            if (project === null) {
                if (!(await transactionManager.existsBy(db_1.Project, { id: projectId }))) {
                    throw new not_found_error_1.NotFoundError('Project not found');
                }
                throw new forbidden_error_1.ForbiddenError("You don't have the permissions to save the credential in this project.");
            }
            const savedCredential = await transactionManager.save(newCredential);
            savedCredential.data = newCredential.data;
            const newSharedCredential = this.sharedCredentialsRepository.create({
                role: 'credential:owner',
                credentials: savedCredential,
                projectId: project.id,
            });
            await transactionManager.save(newSharedCredential);
            if (decryptedCredentialData) {
                await this.credentialDependencyService.upsertExternalSecretProviderDependenciesForCredential({
                    credentialId: savedCredential.id,
                    decryptedCredentialData,
                    entityManager: transactionManager,
                });
            }
            return savedCredential;
        });
        this.logger.debug('New credential created', {
            credentialId: newCredential.id,
            ownerId: user.id,
        });
        return result;
    }
    async delete(user, credentialId, options = {}) {
        const credential = await this.credentialsFinderService.findCredentialForUser(credentialId, user, ['credential:delete'], options);
        if (!credential) {
            return;
        }
        if (credential.isResolvable) {
            const owningProject = await this.sharedCredentialsRepository.findCredentialOwningProject(credentialId);
            await this.ensureCanManageEndUserCredential(user, owningProject?.id);
        }
        await this.externalHooks.run('credentials.delete', [credentialId]);
        if (credential.usageScope === 'instance') {
            const result = await this.credentialsRepository.deleteInstanceCredentialIfUnassigned(credential.id);
            if (result.status === 'assigned') {
                throw new bad_request_error_1.BadRequestError(`This credential is assigned to credential use "${result.credentialUseIds.join(', ')}" and cannot be deleted`);
            }
            return;
        }
        await this.credentialsRepository.remove(credential);
    }
    async test(userId, credentials) {
        return await this.credentialsTester.testCredentials(userId, credentials.type, credentials);
    }
    async testById(userId, credentialId) {
        const storedCredential = await this.credentialsFinderService.findCredentialById(credentialId);
        if (!storedCredential || storedCredential.usageScope !== 'project') {
            throw new credential_not_found_error_1.CredentialNotFoundError(credentialId);
        }
        const credentials = await this.prepareCredentialsForTest({ storedCredential });
        return await this.test(userId, credentials);
    }
    async testWithCredentials(user, credentials) {
        const storedCredential = await this.credentialsFinderService.findCredentialForUser(credentials.id, user, ['credential:read'], { includeInstanceCredentials: true });
        if (!storedCredential) {
            if (credentials.id === '' && (0, permissions_1.hasGlobalScope)(user, 'credential:manageInstance')) {
                this.validateInstanceCredentialData(credentials.data ?? {});
                return await this.test(user.id, credentials);
            }
            throw new credential_not_found_error_1.CredentialNotFoundError(credentials.id);
        }
        const mergedCredentials = await this.prepareCredentialsForTest({
            storedCredential,
            user,
            credentialsToTest: credentials,
        });
        return await this.test(user.id, mergedCredentials);
    }
    redact(data, credential) {
        const copiedData = (0, n8n_workflow_1.deepCopy)(data);
        let credType;
        try {
            credType = this.credentialTypes.getByName(credential.type);
        }
        catch {
            return data;
        }
        const getExtendedProps = (type) => {
            const props = [];
            for (const e of type.extends ?? []) {
                const extendsType = this.credentialTypes.getByName(e);
                const extendedProps = getExtendedProps(extendsType);
                n8n_workflow_1.NodeHelpers.mergeNodeProperties(props, extendedProps);
            }
            n8n_workflow_1.NodeHelpers.mergeNodeProperties(props, type.properties);
            return props;
        };
        const properties = getExtendedProps(credType);
        return this.redactValues(copiedData, properties);
    }
    redactValues(data, props) {
        for (const dataKey of Object.keys(data)) {
            if (dataKey === 'oauthTokenData' || dataKey === 'csrfSecret') {
                if (String(data[dataKey] ?? '').length > 0) {
                    data[dataKey] = n8n_workflow_1.CREDENTIAL_BLANKING_VALUE;
                }
                else {
                    data[dataKey] = n8n_workflow_1.CREDENTIAL_EMPTY_VALUE;
                }
                continue;
            }
            const prop = props.find((v) => v.name === dataKey);
            if (!prop) {
                continue;
            }
            if (prop.type === 'fixedCollection' && prop.options?.length) {
                const dataObject = data[dataKey];
                for (const option of prop.options) {
                    if ((0, n8n_workflow_1.isINodePropertyCollection)(option)) {
                        this.redactCollectionOption(dataObject, option);
                    }
                }
            }
            if (prop.typeOptions?.password &&
                (!String(data[dataKey] ?? '').startsWith('={{') || prop.noDataExpression)) {
                if (String(data[dataKey] ?? '').length > 0) {
                    data[dataKey] = n8n_workflow_1.CREDENTIAL_BLANKING_VALUE;
                }
                else {
                    data[dataKey] = n8n_workflow_1.CREDENTIAL_EMPTY_VALUE;
                }
                continue;
            }
            if (prop.typeOptions?.redactJsonLeaves) {
                const jsonStr = String(data[dataKey] ?? '');
                if (!jsonStr) {
                    data[dataKey] = n8n_workflow_1.CREDENTIAL_EMPTY_VALUE;
                }
                else {
                    try {
                        const parsed = (0, n8n_workflow_1.jsonParse)(jsonStr);
                        data[dataKey] = JSON.stringify(this.redactJsonLeaves(parsed), null, 2);
                    }
                    catch {
                    }
                }
            }
        }
        return data;
    }
    redactCollectionOption(data, option) {
        const collectionValuesKey = option.name;
        const values = data?.[collectionValuesKey];
        if (Array.isArray(values)) {
            for (let i = 0; i < values.length; i++) {
                values[i] = this.redactValues(values[i], option.values);
            }
        }
        else if (typeof values === 'object' && values !== null) {
            data[collectionValuesKey] = this.redactValues(values, option.values);
        }
    }
    redactJsonLeaves(obj) {
        if (Array.isArray(obj))
            return obj.map((item) => this.redactJsonLeaves(item));
        if (typeof obj === 'object' && obj !== null) {
            return Object.fromEntries(Object.entries(obj).map(([k, v]) => [
                k,
                this.redactJsonLeaves(v),
            ]));
        }
        return CUSTOM_AUTH_JSON_REDACTED_VALUE;
    }
    mergeRedactedJsonLeaves(newVal, savedVal) {
        if (newVal === CUSTOM_AUTH_JSON_REDACTED_VALUE)
            return savedVal;
        if (Array.isArray(newVal) && Array.isArray(savedVal)) {
            return newVal.map((item, i) => this.mergeRedactedJsonLeaves(item, savedVal[i]));
        }
        if (Array.isArray(newVal) || Array.isArray(savedVal)) {
            return newVal;
        }
        if (typeof newVal === 'object' &&
            newVal !== null &&
            typeof savedVal === 'object' &&
            savedVal !== null) {
            return Object.fromEntries(Object.entries(newVal).map(([k, v]) => [
                k,
                this.mergeRedactedJsonLeaves(v, savedVal[k]),
            ]));
        }
        return newVal;
    }
    unredactRestoreValues(unmerged, replacement) {
        for (const [key, value] of Object.entries(unmerged)) {
            if (value === n8n_workflow_1.CREDENTIAL_BLANKING_VALUE || value === n8n_workflow_1.CREDENTIAL_EMPTY_VALUE) {
                unmerged[key] = replacement[key];
            }
            else if (typeof value === 'object' &&
                value !== null &&
                key in replacement &&
                typeof replacement[key] === 'object' &&
                replacement[key] !== null) {
                this.unredactRestoreValues(value, replacement[key]);
            }
        }
    }
    getCredentialTypeProperties(credentialType) {
        try {
            return this.credentialTypes.getByName(credentialType).properties;
        }
        catch {
            return [];
        }
    }
    async getChangedSharedFields(credential, newData) {
        try {
            const credentialType = this.credentialTypes.getByName(credential.type);
            const oldData = await this.decrypt(credential, true);
            return (0, shared_fields_1.getChangedSharedFields)(credentialType, oldData, newData);
        }
        catch {
            return [];
        }
    }
    unredact(redactedData, savedData, props = []) {
        const mergedData = (0, n8n_workflow_1.deepCopy)(redactedData);
        this.unredactRestoreValues(mergedData, savedData);
        for (const prop of props) {
            if (!prop.typeOptions?.redactJsonLeaves)
                continue;
            if (!(prop.name in mergedData) || !(prop.name in savedData))
                continue;
            try {
                const newJson = (0, n8n_workflow_1.jsonParse)(String(mergedData[prop.name]));
                const savedJson = (0, n8n_workflow_1.jsonParse)(String(savedData[prop.name]));
                mergedData[prop.name] = (0, n8n_workflow_1.jsonStringify)(this.mergeRedactedJsonLeaves(newJson, savedJson));
            }
            catch {
            }
        }
        return mergedData;
    }
    async getOne(user, credentialId, includeDecryptedData, options = {}) {
        if (options.includeInstanceCredentials && (0, permissions_1.hasGlobalScope)(user, 'credential:manageInstance')) {
            const instanceCredential = await this.credentialsRepository.findOneBy({
                id: credentialId,
                usageScope: 'instance',
            });
            if (instanceCredential) {
                const { data: _, ...rest } = instanceCredential;
                if (includeDecryptedData) {
                    const decryptedData = await this.decrypt(instanceCredential);
                    if (decryptedData.oauthTokenData) {
                        decryptedData.oauthTokenData = true;
                    }
                    return { data: decryptedData, ...rest };
                }
                return { ...rest };
            }
        }
        let sharing = null;
        let decryptedData = null;
        sharing = includeDecryptedData
            ?
                await this.getSharing(user, credentialId, [
                    'credential:read',
                ])
            : null;
        if (sharing) {
            decryptedData = await this.decrypt(sharing.credentials);
        }
        else {
            sharing = await this.getSharing(user, credentialId, ['credential:read']);
        }
        if (!sharing) {
            throw new not_found_error_1.NotFoundError(`Credential with ID "${credentialId}" could not be found.`);
        }
        const { credentials: credential } = sharing;
        const { data: _, ...rest } = credential;
        const enriched = rest;
        await this.populateConnectedByMe([enriched], user);
        if (credential.isResolvable) {
            enriched.connectedUserCount = await this.countConnectedUsers(credential.id);
        }
        if (decryptedData) {
            if (credential.isResolvable) {
                if (enriched.connectedByMe) {
                    decryptedData.oauthTokenData = true;
                }
                else {
                    delete decryptedData.oauthTokenData;
                }
            }
            else if (decryptedData?.oauthTokenData) {
                decryptedData.oauthTokenData = true;
            }
            return { data: decryptedData, ...enriched };
        }
        return { ...enriched };
    }
    async getCredentialScopes(user, credentialId) {
        const userProjectRelations = await this.projectService.getProjectRelationsForUser(user);
        const projectIds = [...new Set(userProjectRelations.map((pr) => pr.projectId))];
        if (projectIds.length === 0) {
            return this.roleService.combineResourceScopes('credential', user, [], userProjectRelations);
        }
        const shared = await this.sharedCredentialsRepository.find({
            where: {
                projectId: (0, typeorm_1.In)(projectIds),
                credentialsId: credentialId,
            },
        });
        return this.roleService.combineResourceScopes('credential', user, shared, userProjectRelations);
    }
    async transferAll(fromProjectId, toProjectId, trx) {
        trx = trx ?? this.credentialsRepository.manager;
        const allSharedCredentials = await trx.findBy(db_1.SharedCredentials, {
            projectId: (0, typeorm_1.In)([fromProjectId, toProjectId]),
        });
        const sharedCredentialsOfFromProject = allSharedCredentials.filter((sc) => sc.projectId === fromProjectId);
        const ownedCredentialIds = sharedCredentialsOfFromProject
            .filter((sc) => sc.role === 'credential:owner')
            .map((sc) => sc.credentialsId);
        await this.sharedCredentialsRepository.makeOwner(ownedCredentialIds, toProjectId, trx);
        await this.sharedCredentialsRepository.deleteByIds(ownedCredentialIds, fromProjectId, trx);
        const sharedCredentialIdsOfTransferee = allSharedCredentials
            .filter((sc) => sc.projectId === toProjectId)
            .map((sc) => sc.credentialsId);
        const sharedCredentialsToTransfer = sharedCredentialsOfFromProject.filter((sc) => sc.role !== 'credential:owner' &&
            !sharedCredentialIdsOfTransferee.includes(sc.credentialsId));
        await trx.insert(db_1.SharedCredentials, sharedCredentialsToTransfer.map((sc) => ({
            credentialsId: sc.credentialsId,
            projectId: toProjectId,
            role: sc.role,
        })));
    }
    async replaceCredentialContentsForSharee(user, credential, decryptedData, mergedCredentials) {
        if (!(await (0, check_access_1.userHasScopes)(user, ['credential:update'], false, { credentialId: credential.id }))) {
            mergedCredentials.data = decryptedData;
        }
    }
    async createUnmanagedCredential(dto, user) {
        return await this.createCredential({ ...dto, isManaged: false }, user);
    }
    async createInstanceCredential(dto, user, ctx, options = {}) {
        if (dto.usageScope !== 'instance') {
            throw new bad_request_error_1.BadRequestError('Provider connections must use instance usage scope');
        }
        return await this.persistInstanceCredential({ ...dto, isManaged: false }, user, ctx, options);
    }
    async createStubCredential(opts, user) {
        const encryptedCredential = await this.createEncryptedData({
            id: null,
            name: opts.name,
            type: opts.type,
            data: {},
        });
        const credentialEntity = this.credentialsRepository.create({
            ...encryptedCredential,
            isManaged: false,
            isResolvable: false,
        });
        return await this.save(credentialEntity, encryptedCredential, user, opts.projectId, {});
    }
    async checkCredentialData(type, data, user, projectId) {
        this.validateCredentialData(type, data);
        await (0, validation_1.validateExternalSecretsPermissions)({ user, projectId, dataToSave: data });
    }
    validateCredentialData(type, data) {
        const credentialProperties = this.credentialsHelper.getCredentialsProperties(type);
        for (const property of credentialProperties) {
            if (property.required &&
                property.displayOptions !== undefined &&
                (0, n8n_workflow_1.displayParameter)(data, property, null, null)) {
                const value = data[property.name];
                const hasDefault = property.default !== undefined && property.default !== null && property.default !== '';
                if ((value === undefined || value === null || value === '') && !hasDefault) {
                    throw new bad_request_error_1.BadRequestError(`The field "${property.name}" is mandatory for credentials of type "${type}"`);
                }
            }
        }
        this.validateOAuthCredentialUrls(type, data);
    }
    isOAuthCredentialType(type) {
        const parentTypes = this.credentialTypes.getParentTypes(type) ?? [];
        return (type === 'oAuth1Api' ||
            type === 'oAuth2Api' ||
            parentTypes.includes('oAuth1Api') ||
            parentTypes.includes('oAuth2Api'));
    }
    validateOAuthCredentialUrls(type, data) {
        const parentTypes = this.credentialTypes.getParentTypes(type) ?? [];
        const isOAuth2 = type === 'oAuth2Api' || parentTypes.includes('oAuth2Api');
        const isOAuth1 = type === 'oAuth1Api' || parentTypes.includes('oAuth1Api');
        if (isOAuth2) {
            const oauthUrlFields = ['authUrl', 'accessTokenUrl', 'serverUrl'];
            for (const field of oauthUrlFields) {
                const value = data[field];
                if (typeof value === 'string' && value.trim() !== '' && !(0, n8n_workflow_1.isExpression)(value)) {
                    (0, validate_oauth_url_1.validateOAuthUrl)(value);
                }
            }
        }
        if (isOAuth1) {
            const oauthUrlFields = ['authUrl', 'requestTokenUrl', 'accessTokenUrl'];
            for (const field of oauthUrlFields) {
                const value = data[field];
                if (typeof value === 'string' && value.trim() !== '' && !(0, n8n_workflow_1.isExpression)(value)) {
                    (0, validate_oauth_url_1.validateOAuthUrl)(value);
                }
            }
        }
    }
    async createManagedCredential(dto, user) {
        return await this.createCredential({ ...dto, isManaged: true }, user);
    }
    async ensureCanManageEndUserCredential(user, projectId) {
        const allowed = projectId !== undefined &&
            (await (0, check_access_1.userHasScopes)(user, ['credential:createEndUser'], false, { projectId }));
        if (!allowed) {
            throw new forbidden_error_1.ForbiddenError('You do not have permission to manage end-user credentials in this project');
        }
    }
    async createCredential(opts, user) {
        if (opts.usageScope === 'instance') {
            const savedCredential = await this.persistInstanceCredential(opts, user, {});
            const scopes = await this.getCredentialScopes(user, savedCredential.id);
            const { shared, ...credential } = savedCredential;
            return { ...credential, scopes };
        }
        const targetProjectId = await this.resolveOwningProjectIdForNewCredential(user, opts.projectId);
        if (opts.isResolvable === true) {
            await this.ensureCanManageEndUserCredential(user, targetProjectId);
        }
        await this.checkCredentialData(opts.type, opts.data, user, targetProjectId);
        if (this.externalSecretsConfig.externalSecretsForProjects) {
            await (0, validation_1.validateAccessToReferencedSecretProviders)(targetProjectId, opts.data, this.externalSecretsProviderAccessCheckService, 'create');
        }
        const encryptedCredential = await this.createEncryptedData({
            id: null,
            name: opts.name,
            type: opts.type,
            data: opts.data,
        });
        const isGlobal = opts.isGlobal;
        if (isGlobal === true) {
            const canShareGlobally = (0, permissions_1.hasGlobalScope)(user, 'credential:shareGlobally');
            if (!canShareGlobally) {
                throw new forbidden_error_1.ForbiddenError('You do not have permission to create globally shared credentials');
            }
            encryptedCredential.isGlobal = isGlobal;
        }
        const credentialEntity = this.credentialsRepository.create({
            ...encryptedCredential,
            isManaged: opts.isManaged,
            isResolvable: opts.isResolvable ?? false,
        });
        const { shared, ...credential } = await this.save(credentialEntity, encryptedCredential, user, targetProjectId, opts.data);
        const scopes = await this.getCredentialScopes(user, credential.id);
        return { ...credential, scopes };
    }
    async persistInstanceCredential(opts, user, ctx, options = {}) {
        if (!(0, permissions_1.hasGlobalScope)(user, 'credential:manageInstance')) {
            throw new forbidden_error_1.ForbiddenError('You do not have permission to create provider connections');
        }
        if (opts.isGlobal === true) {
            throw new bad_request_error_1.BadRequestError('Provider connections cannot be globally shared');
        }
        if (opts.isResolvable === true || opts.isManaged) {
            throw new bad_request_error_1.BadRequestError('Provider connections cannot be end-user or managed credentials');
        }
        const data = opts.data;
        this.validateInstanceCredentialData(data);
        this.validateCredentialData(opts.type, data);
        const encryptedCredential = options.encryptedData ??
            (await this.createEncryptedData({
                id: null,
                name: opts.name,
                type: opts.type,
                data,
            }));
        if (!options.skipExternalHooks) {
            await this.externalHooks.run('credentials.create', [encryptedCredential]);
        }
        const hookedData = await this.getValidatedInstanceCredentialHookData(encryptedCredential, null, opts.type);
        this.validateInstanceCredentialData(hookedData);
        this.validateCredentialData(opts.type, hookedData);
        const credentialEntity = this.credentialsRepository.create({
            ...encryptedCredential,
            isManaged: false,
            isResolvable: false,
            usageScope: 'instance',
        });
        const savedCredential = await this.credentialsRepository.saveInstanceCredential(credentialEntity, ctx);
        this.logger.debug('New instance credential created', {
            credentialId: savedCredential.id,
            ownerId: user.id,
        });
        return savedCredential;
    }
    async getValidatedInstanceCredentialHookData(credential, expectedId, expectedType) {
        if ((credential.id ?? null) !== expectedId) {
            throw new bad_request_error_1.BadRequestError('Provider connection hooks cannot change the credential ID');
        }
        if (credential.type !== expectedType) {
            throw new bad_request_error_1.BadRequestError('Provider connection hooks cannot change the credential type');
        }
        if (credential.isGlobal ||
            credential.isManaged ||
            credential.isResolvable ||
            credential.resolvableAllowFallback ||
            (credential.resolverId !== undefined && credential.resolverId !== null) ||
            (credential.usageScope !== undefined && credential.usageScope !== 'instance')) {
            throw new bad_request_error_1.BadRequestError('Provider connection hooks cannot change the credential scope');
        }
        try {
            return await (0, credentials_helper_1.createCredentialsFromCredentialsEntity)(credential).getData();
        }
        catch (error) {
            if (error instanceof n8n_core_1.CredentialDataError) {
                throw new bad_request_error_1.BadRequestError('Provider connection hooks returned invalid credential data');
            }
            throw error;
        }
    }
    validateInstanceCredentialData(data) {
        if ((0, external_secrets_utils_1.getExternalSecretExpressionPaths)(data).length > 0) {
            throw new bad_request_error_1.BadRequestError('Provider connections cannot reference project-scoped external secrets');
        }
    }
    async validateInstanceCredentialUpdate(credential, data, credentialUseIds, ctx = {}) {
        this.validateInstanceCredentialData(data);
        const assignedUseIds = credentialUseIds ??
            (await this.instanceCredentialAssignmentRepository.findCredentialUseIds(credential.id, ctx));
        for (const credentialUseId of assignedUseIds) {
            const credentialUse = this.instanceCredentialUseRegistry.get(credentialUseId);
            if (!credentialUse.credentialTypes.includes(credential.type)) {
                throw new bad_request_error_1.BadRequestError(`Credential type "${credential.type}" is not valid for instance credential use "${credentialUseId}"`);
            }
            credentialUse.validate?.({ type: credential.type, data });
        }
    }
    async prepareCredentialsForTest({ storedCredential, user, credentialsToTest, }) {
        const decryptedData = await this.decrypt(storedCredential, true);
        const mergedCredentials = credentialsToTest
            ? (0, n8n_workflow_1.deepCopy)(credentialsToTest)
            : {
                id: storedCredential.id,
                name: storedCredential.name,
                type: storedCredential.type,
                data: decryptedData,
            };
        if (user && credentialsToTest) {
            await this.replaceCredentialContentsForSharee(user, storedCredential, decryptedData, mergedCredentials);
            if (mergedCredentials.data) {
                mergedCredentials.data = this.unredact(mergedCredentials.data, decryptedData, this.getCredentialTypeProperties(storedCredential.type));
            }
        }
        return mergedCredentials;
    }
};
exports.CredentialsService = CredentialsService;
exports.CredentialsService = CredentialsService = __decorate([
    (0, di_1.Service)(),
    __metadata("design:paramtypes", [db_1.CredentialsRepository, credential_dependency_service_1.CredentialDependencyService, db_1.SharedCredentialsRepository, ownership_service_1.OwnershipService, backend_common_1.Logger, n8n_core_1.ErrorReporter, credentials_tester_service_1.CredentialsTester, external_hooks_1.ExternalHooks, credential_types_1.CredentialTypes, db_1.ProjectRepository, project_service_ee_1.ProjectService, role_service_1.RoleService, db_1.UserRepository, credentials_finder_service_1.CredentialsFinderService, credentials_helper_1.CredentialsHelper, external_secrets_config_1.ExternalSecretsConfig, secret_provider_access_check_service_ee_1.SecretsProviderAccessCheckService, credential_connection_status_proxy_1.CredentialConnectionStatusProxy, db_1.InstanceCredentialAssignmentRepository, instance_credential_use_registry_1.InstanceCredentialUseRegistry, db_1.DbLockService])
], CredentialsService);
//# sourceMappingURL=credentials.service.js.map