mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
feat(core): Split private credentials behind a dedicated release flag (no-changelog) (#32332)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
1b6a3bd814
commit
e978fa8135
@@ -3,20 +3,43 @@ import type { ModuleInterface } from '@n8n/decorators';
|
||||
import { BackendModule, OnShutdown } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
function isFeatureFlagEnabled(): boolean {
|
||||
/**
|
||||
* Base capability: per-user "private credentials" resolved through the seeded
|
||||
* system resolver. Enabled by either the private-credentials flag or the
|
||||
* superset dynamic-credentials flag.
|
||||
*/
|
||||
function isPrivateCredentialsEnabled(): boolean {
|
||||
return (
|
||||
process.env.N8N_ENV_FEAT_PRIVATE_CREDENTIALS === 'true' ||
|
||||
process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS === 'true'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Superset capability: external/custom credential resolvers (OAuth/Slack) plus
|
||||
* their management surfaces and identity-extractor hooks.
|
||||
*/
|
||||
function isExternalResolversEnabled(): boolean {
|
||||
return process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS === 'true';
|
||||
}
|
||||
|
||||
@BackendModule({ name: 'dynamic-credentials', licenseFlag: LICENSE_FEATURES.DYNAMIC_CREDENTIALS })
|
||||
export class DynamicCredentialsModule implements ModuleInterface {
|
||||
async init() {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
if (!isPrivateCredentialsEnabled()) {
|
||||
return;
|
||||
}
|
||||
await import('./dynamic-credentials.controller');
|
||||
await import('./credential-resolvers.controller');
|
||||
await import('./context-establishment-hooks');
|
||||
await import('./credential-resolvers');
|
||||
|
||||
// System resolver powers private credentials; OAuth/Slack resolvers and
|
||||
// their management/identity-extractor surfaces are external-only.
|
||||
await import('./credential-resolvers/n8n-credential-resolver');
|
||||
if (isExternalResolversEnabled()) {
|
||||
await import('./credential-resolvers.controller');
|
||||
await import('./context-establishment-hooks');
|
||||
await import('./credential-resolvers/oauth-credential-resolver');
|
||||
await import('./credential-resolvers/slack-credential-resolver');
|
||||
}
|
||||
const {
|
||||
DynamicCredentialResolverRegistry,
|
||||
DynamicCredentialStorageService,
|
||||
@@ -48,7 +71,7 @@ export class DynamicCredentialsModule implements ModuleInterface {
|
||||
}
|
||||
|
||||
async entities() {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
if (!isPrivateCredentialsEnabled()) {
|
||||
return [];
|
||||
}
|
||||
const { DynamicCredentialResolver } = await import('./database/entities/credential-resolver');
|
||||
@@ -61,7 +84,7 @@ export class DynamicCredentialsModule implements ModuleInterface {
|
||||
}
|
||||
|
||||
async context() {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
if (!isPrivateCredentialsEnabled()) {
|
||||
return {};
|
||||
}
|
||||
const { CredentialCheckProxyService } = await import(
|
||||
|
||||
@@ -49,7 +49,7 @@ import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
|
||||
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
|
||||
import { useWorkflowActivate } from '@/app/composables/useWorkflowActivate';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
import { useDependencies } from '@/app/composables/useDependencies';
|
||||
|
||||
const WORKFLOW_LIST_ITEM_ACTIONS = {
|
||||
@@ -117,7 +117,7 @@ const locale = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const telemetry = useTelemetry();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
|
||||
const { hasDependencies } = useDependencies();
|
||||
|
||||
const uiStore = useUIStore();
|
||||
@@ -318,7 +318,7 @@ const isWorkflowPublished = computed(() => {
|
||||
});
|
||||
|
||||
const hasDynamicCredentials = computed(() => {
|
||||
return isDynamicCredentialsEnabled.value && props.data.hasResolvableCredentials;
|
||||
return isPrivateCredentialsEnabled.value && props.data.hasResolvableCredentials;
|
||||
});
|
||||
|
||||
const workflowHasDependencies = computed(() => hasDependencies(props.data.id));
|
||||
|
||||
@@ -25,13 +25,13 @@ import type {
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
|
||||
vi.mock('@/features/resolvers/composables/useDynamicCredentials', () => ({
|
||||
useDynamicCredentials: vi.fn(),
|
||||
vi.mock('@/features/resolvers/composables/usePrivateCredentials', () => ({
|
||||
usePrivateCredentials: vi.fn(),
|
||||
}));
|
||||
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
|
||||
const mockedUseDynamicCredentials = vi.mocked(useDynamicCredentials);
|
||||
const mockedUseDynamicCredentials = vi.mocked(usePrivateCredentials);
|
||||
|
||||
const mockDocumentStoreUsedCredentials: Record<string, IUsedCredential> = {};
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('useNodeHelpers()', () => {
|
||||
beforeEach(() => {
|
||||
mockedUseDynamicCredentials.mockReturnValue({
|
||||
isEnabled: computed(() => true),
|
||||
} as ReturnType<typeof useDynamicCredentials>);
|
||||
} as ReturnType<typeof usePrivateCredentials>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -980,7 +980,7 @@ describe('useNodeHelpers()', () => {
|
||||
it('emits no issue when dynamic credentials feature is disabled', () => {
|
||||
mockedUseDynamicCredentials.mockReturnValue({
|
||||
isEnabled: computed(() => false),
|
||||
} as ReturnType<typeof useDynamicCredentials>);
|
||||
} as ReturnType<typeof usePrivateCredentials>);
|
||||
|
||||
const cred = makePrivateCred({ connectedByMe: false });
|
||||
mockedStore(useCredentialsStore).getCredentialById = vi.fn().mockReturnValue(cred);
|
||||
@@ -1108,7 +1108,7 @@ describe('useNodeHelpers()', () => {
|
||||
it('does not warn when dynamic credentials feature is disabled', () => {
|
||||
mockedUseDynamicCredentials.mockReturnValue({
|
||||
isEnabled: computed(() => false),
|
||||
} as ReturnType<typeof useDynamicCredentials>);
|
||||
} as ReturnType<typeof usePrivateCredentials>);
|
||||
mockConnectedPrivateCred(true);
|
||||
mockDocumentStore.workflowTriggerNodes = [buildTriggerNode(WEBHOOK_TRIGGER)];
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ import { useCanvasStore } from '@/app/stores/canvas.store';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
|
||||
declare namespace HttpRequestNode {
|
||||
namespace V2 {
|
||||
@@ -74,7 +74,7 @@ export function useNodeHelpers() {
|
||||
const canvasStore = useCanvasStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
|
||||
|
||||
const isInsertingNodes = ref(false);
|
||||
const credentialsUpdated = ref(false);
|
||||
@@ -434,7 +434,7 @@ export function useNodeHelpers() {
|
||||
node: INodeUi,
|
||||
foundIssues: INodeIssueObjectProperty,
|
||||
): void {
|
||||
if (!isDynamicCredentialsEnabled.value) return;
|
||||
if (!isPrivateCredentialsEnabled.value) return;
|
||||
|
||||
const incompatibleTrigger = workflowHasIncompatibleTrigger();
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ import DynamicCredentialsDrawer from './components/DynamicCredentialsDrawer.vue'
|
||||
import { useChatArtifacts } from './composables/useChatArtifacts';
|
||||
import { useChatInputFocus } from './composables/useChatInputFocus';
|
||||
import { useDynamicCredentialsStatus } from './composables/useDynamicCredentialsStatus';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -241,9 +241,9 @@ const { credentialsByProvider, selectCredential } = useChatCredentials(
|
||||
);
|
||||
|
||||
// Dynamic credentials
|
||||
const { isEnabled: dynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: privateCredentialsEnabled } = usePrivateCredentials();
|
||||
const dynamicCredsWorkflowId = computed(() =>
|
||||
selectedModel.value?.model.provider === 'n8n' && dynamicCredentialsEnabled.value
|
||||
selectedModel.value?.model.provider === 'n8n' && privateCredentialsEnabled.value
|
||||
? selectedModel.value.model.workflowId
|
||||
: null,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import DependencyPill from '@/app/components/DependencyPill.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { ResourceType } from '@/features/collaboration/projects/projects.utils';
|
||||
import type { CredentialsResource } from '@/Interface';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
import { useCredentialOAuth } from '../composables/useCredentialOAuth';
|
||||
|
||||
import {
|
||||
@@ -59,7 +59,7 @@ const toast = useToast();
|
||||
const uiStore = useUIStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
|
||||
const { hasDependencies } = useDependencies();
|
||||
const { authorize, isOAuthCredentialType } = useCredentialOAuth();
|
||||
|
||||
@@ -73,7 +73,7 @@ const credentialPermissions = computed(() => getResourcePermissions(props.data.s
|
||||
|
||||
const isPrivateUnconnected = computed(
|
||||
() =>
|
||||
isDynamicCredentialsEnabled.value &&
|
||||
isPrivateCredentialsEnabled.value &&
|
||||
props.data.isResolvable === true &&
|
||||
props.data.connectedByMe === false &&
|
||||
credentialPermissions.value.update === true,
|
||||
@@ -101,7 +101,7 @@ const actions = computed(() => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDynamicCredentialsEnabled.value && props.data.isResolvable && props.data.connectedByMe) {
|
||||
if (isPrivateCredentialsEnabled.value && props.data.isResolvable && props.data.connectedByMe) {
|
||||
items.push({
|
||||
label: locale.baseText('credentials.item.disconnect'),
|
||||
value: CREDENTIAL_LIST_ITEM_ACTIONS.DISCONNECT,
|
||||
@@ -238,7 +238,7 @@ function moveResource() {
|
||||
<N8nBadge v-if="needsSetup" class="ml-3xs" theme="warning">
|
||||
{{ locale.baseText('credentials.item.needsSetup') }}
|
||||
</N8nBadge>
|
||||
<N8nTooltip v-if="isDynamicCredentialsEnabled && data.isResolvable" placement="top">
|
||||
<N8nTooltip v-if="isPrivateCredentialsEnabled && data.isResolvable" placement="top">
|
||||
<template #content>
|
||||
<div :class="$style.tooltipContent">
|
||||
<strong>{{ locale.baseText('credentials.private.tooltipTitle') }}</strong>
|
||||
|
||||
+13
-13
@@ -177,7 +177,7 @@ describe('CredentialConfig', () => {
|
||||
});
|
||||
|
||||
describe('Dynamic Credentials Section', () => {
|
||||
it('should not display dynamic credentials section when isDynamicCredentialsEnabled is false', async () => {
|
||||
it('should not display dynamic credentials section when isPrivateCredentialsEnabled is false', async () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
isManaged: false,
|
||||
@@ -185,7 +185,7 @@ describe('CredentialConfig', () => {
|
||||
credentialType: mockCredentialType,
|
||||
credentialProperties: [],
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
isDynamicCredentialsEnabled: false,
|
||||
isPrivateCredentialsEnabled: false,
|
||||
isOAuthType: true,
|
||||
isNewCredential: true,
|
||||
credentialPermissions: {
|
||||
@@ -211,7 +211,7 @@ describe('CredentialConfig', () => {
|
||||
credentialType: mockCredentialType,
|
||||
credentialProperties: [],
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isOAuthType: false,
|
||||
isNewCredential: true,
|
||||
credentialPermissions: {
|
||||
@@ -237,7 +237,7 @@ describe('CredentialConfig', () => {
|
||||
credentialType: mockCredentialType,
|
||||
credentialProperties: [],
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isOAuthType: true,
|
||||
isNewCredential: true,
|
||||
credentialPermissions: {
|
||||
@@ -263,7 +263,7 @@ describe('CredentialConfig', () => {
|
||||
credentialType: mockCredentialType,
|
||||
credentialProperties: [],
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isOAuthType: true,
|
||||
isNewCredential: false,
|
||||
credentialPermissions: {
|
||||
@@ -289,7 +289,7 @@ describe('CredentialConfig', () => {
|
||||
credentialType: mockCredentialType,
|
||||
credentialProperties: [],
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isOAuthType: true,
|
||||
isNewCredential: true,
|
||||
isResolvable: false,
|
||||
@@ -317,7 +317,7 @@ describe('CredentialConfig', () => {
|
||||
credentialType: mockCredentialType,
|
||||
credentialProperties: [],
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isOAuthType: true,
|
||||
isNewCredential: false,
|
||||
isResolvable: false,
|
||||
@@ -345,7 +345,7 @@ describe('CredentialConfig', () => {
|
||||
credentialType: mockCredentialType,
|
||||
credentialProperties: [],
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isOAuthType: true,
|
||||
isNewCredential: false,
|
||||
isResolvable: false,
|
||||
@@ -393,7 +393,7 @@ describe('CredentialConfig', () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
...oAuthConnectedProps,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isResolvable: true,
|
||||
connectedByMe: true,
|
||||
},
|
||||
@@ -406,7 +406,7 @@ describe('CredentialConfig', () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
...oAuthConnectedProps,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isResolvable: true,
|
||||
connectedByMe: false,
|
||||
},
|
||||
@@ -419,7 +419,7 @@ describe('CredentialConfig', () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
...oAuthConnectedProps,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isResolvable: false,
|
||||
connectedByMe: true,
|
||||
},
|
||||
@@ -432,7 +432,7 @@ describe('CredentialConfig', () => {
|
||||
renderComponent({
|
||||
props: {
|
||||
...oAuthConnectedProps,
|
||||
isDynamicCredentialsEnabled: false,
|
||||
isPrivateCredentialsEnabled: false,
|
||||
isResolvable: true,
|
||||
connectedByMe: true,
|
||||
},
|
||||
@@ -445,7 +445,7 @@ describe('CredentialConfig', () => {
|
||||
const { emitted } = renderComponent({
|
||||
props: {
|
||||
...oAuthConnectedProps,
|
||||
isDynamicCredentialsEnabled: true,
|
||||
isPrivateCredentialsEnabled: true,
|
||||
isResolvable: true,
|
||||
connectedByMe: true,
|
||||
},
|
||||
|
||||
+3
-3
@@ -66,7 +66,7 @@ type Props = {
|
||||
isRetesting?: boolean;
|
||||
requiredPropertiesFilled?: boolean;
|
||||
isManaged?: boolean;
|
||||
isDynamicCredentialsEnabled?: boolean;
|
||||
isPrivateCredentialsEnabled?: boolean;
|
||||
isResolvable?: boolean;
|
||||
isShared?: boolean;
|
||||
connectedByMe?: boolean;
|
||||
@@ -209,7 +209,7 @@ const showOAuthNotConnectedBanner = computed(() => {
|
||||
});
|
||||
|
||||
const showDisconnectButton = computed(
|
||||
() => !!props.isDynamicCredentialsEnabled && !!props.isResolvable && !!props.connectedByMe,
|
||||
() => !!props.isPrivateCredentialsEnabled && !!props.isResolvable && !!props.connectedByMe,
|
||||
);
|
||||
|
||||
const isMissingCredentials = computed(() => props.credentialType === null);
|
||||
@@ -452,7 +452,7 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
||||
|
||||
<div
|
||||
v-if="
|
||||
isDynamicCredentialsEnabled &&
|
||||
isPrivateCredentialsEnabled &&
|
||||
// Only OAuth credentials can be dynamic for now, as they are the only ones with the managed authorize endpoint
|
||||
isOAuthType &&
|
||||
canWrite
|
||||
|
||||
+2
-2
@@ -48,9 +48,9 @@ vi.mock('@/app/composables/useMessage', () => ({
|
||||
useMessage: () => ({ confirm: confirmMock }),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/resolvers/composables/useDynamicCredentials', async () => {
|
||||
vi.mock('@/features/resolvers/composables/usePrivateCredentials', async () => {
|
||||
const { ref } = await vi.importActual<typeof import('vue')>('vue');
|
||||
return { useDynamicCredentials: () => ({ isEnabled: ref(true) }) };
|
||||
return { usePrivateCredentials: () => ({ isEnabled: ref(true) }) };
|
||||
});
|
||||
|
||||
const oAuth2Api: ICredentialType = {
|
||||
|
||||
+3
-3
@@ -67,7 +67,7 @@ import {
|
||||
} from '@n8n/design-system';
|
||||
import { setParameterValue } from '@/app/utils/parameterUtils';
|
||||
import get from 'lodash/get';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
import { useQuickConnect } from '../../quickConnect/composables/useQuickConnect';
|
||||
import type { CredentialModeOption } from './CredentialModeSelector.vue';
|
||||
|
||||
@@ -124,7 +124,7 @@ async function confirmModal(
|
||||
const telemetry = useTelemetry();
|
||||
const router = useRouter();
|
||||
const rootStore = useRootStore();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
|
||||
const { getQuickConnectOption, connect: quickConnect } = useQuickConnect();
|
||||
const isQuickConnectMode = ref(false);
|
||||
const activeTab = ref('connection');
|
||||
@@ -1680,7 +1680,7 @@ const { width } = useElementSize(credNameRef);
|
||||
:credential-permissions="credentialPermissions"
|
||||
:mode="mode"
|
||||
:selected-credential="selectedCredential"
|
||||
:is-dynamic-credentials-enabled="isDynamicCredentialsEnabled"
|
||||
:is-private-credentials-enabled="isPrivateCredentialsEnabled"
|
||||
:is-resolvable="isResolvable"
|
||||
:is-shared="isCurrentlyShared"
|
||||
:connected-by-me="connectedByMe"
|
||||
|
||||
@@ -37,7 +37,7 @@ import { assert } from '@n8n/utils/assert';
|
||||
import { isEmpty } from '@/app/utils/typesUtils';
|
||||
import { getResourcePermissions } from '@n8n/permissions';
|
||||
import { useNodeCredentialOptions } from '../composables/useNodeCredentialOptions';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
import { SYSTEM_RESOLVER_ID } from '@n8n/api-types';
|
||||
import { useAiGateway } from '@/app/composables/useAiGateway';
|
||||
import AiGatewaySelector from '@/app/components/AiGatewaySelector.vue';
|
||||
@@ -100,7 +100,7 @@ const uiStore = useUIStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = props.standalone ? undefined : injectWorkflowDocumentStore();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
|
||||
|
||||
// Quick connect
|
||||
const {
|
||||
@@ -163,7 +163,7 @@ const selected = computed<Record<string, INodeCredentialsDetails>>(
|
||||
);
|
||||
|
||||
function isCredentialResolvable(credentialType: string): boolean {
|
||||
if (!isDynamicCredentialsEnabled.value) return false;
|
||||
if (!isPrivateCredentialsEnabled.value) return false;
|
||||
const credentialId = selected.value[credentialType]?.id;
|
||||
if (!credentialId) return false;
|
||||
const credential = credentialsStore.getCredentialById(credentialId);
|
||||
@@ -171,7 +171,7 @@ function isCredentialResolvable(credentialType: string): boolean {
|
||||
}
|
||||
|
||||
function getSelectedPrivateCredential(credentialType: string): ICredentialsResponse | null {
|
||||
if (!isDynamicCredentialsEnabled.value) return null;
|
||||
if (!isPrivateCredentialsEnabled.value) return null;
|
||||
const id = selected.value[credentialType]?.id;
|
||||
if (!id) return null;
|
||||
const credential = credentialsStore.getCredentialById(id);
|
||||
@@ -841,7 +841,7 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
|
||||
<div :class="$style.credentialOptionName">
|
||||
<N8nText bold>{{ item.name }}</N8nText>
|
||||
<N8nTooltip
|
||||
v-if="isDynamicCredentialsEnabled && item.isResolvable"
|
||||
v-if="isPrivateCredentialsEnabled && item.isResolvable"
|
||||
placement="top"
|
||||
>
|
||||
<template #content>{{
|
||||
|
||||
+8
@@ -2,6 +2,14 @@ import { computed } from 'vue';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
|
||||
/**
|
||||
* Gates the external/custom credential resolver surfaces (resolver management
|
||||
* page, workflow-level resolver dropdown, custom resolver create/edit). This is
|
||||
* the superset capability.
|
||||
*
|
||||
* For the base "private credentials" surfaces (per-user self-connect via the
|
||||
* system resolver) use `usePrivateCredentials`.
|
||||
*/
|
||||
export const useDynamicCredentials = () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const { check } = useEnvFeatureFlag();
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { usePrivateCredentials } from './usePrivateCredentials';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
|
||||
describe('usePrivateCredentials', () => {
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
|
||||
const setup = ({
|
||||
moduleActive,
|
||||
privateFlag,
|
||||
dynamicFlag,
|
||||
}: {
|
||||
moduleActive: boolean;
|
||||
privateFlag: boolean;
|
||||
dynamicFlag: boolean;
|
||||
}) => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
settingsStore = useSettingsStore();
|
||||
settingsStore.settings = {
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_PRIVATE_CREDENTIALS: privateFlag,
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: dynamicFlag,
|
||||
},
|
||||
activeModules: moduleActive ? ['dynamic-credentials'] : [],
|
||||
} as unknown as FrontendSettings;
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockImplementation(
|
||||
(name: string) => moduleActive && name === 'dynamic-credentials',
|
||||
);
|
||||
|
||||
return usePrivateCredentials();
|
||||
};
|
||||
|
||||
it('should be enabled when module is active and the private flag is on', () => {
|
||||
const { isEnabled } = setup({ moduleActive: true, privateFlag: true, dynamicFlag: false });
|
||||
expect(isEnabled.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should be enabled when module is active and only the superset dynamic flag is on', () => {
|
||||
const { isEnabled } = setup({ moduleActive: true, privateFlag: false, dynamicFlag: true });
|
||||
expect(isEnabled.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should be disabled when module is not active', () => {
|
||||
const { isEnabled } = setup({ moduleActive: false, privateFlag: true, dynamicFlag: true });
|
||||
expect(isEnabled.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should be disabled when neither flag is on', () => {
|
||||
const { isEnabled } = setup({ moduleActive: true, privateFlag: false, dynamicFlag: false });
|
||||
expect(isEnabled.value).toBe(false);
|
||||
});
|
||||
});
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { computed } from 'vue';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
|
||||
/**
|
||||
* Gates the "private credentials" surfaces (per-user self-connect via the seeded
|
||||
* system resolver). This is the base capability and is enabled by either the
|
||||
* `PRIVATE_CREDENTIALS` flag or the superset `DYNAMIC_CREDENTIALS` flag.
|
||||
*
|
||||
* For the external/custom resolver surfaces (resolver management, workflow
|
||||
* resolver dropdown, custom resolver create/edit) use `useDynamicCredentials`.
|
||||
*/
|
||||
export const usePrivateCredentials = () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const { check } = useEnvFeatureFlag();
|
||||
|
||||
const isEnabled = computed(
|
||||
() =>
|
||||
settingsStore.isModuleActive('dynamic-credentials') &&
|
||||
(check.value('PRIVATE_CREDENTIALS') || check.value('DYNAMIC_CREDENTIALS')),
|
||||
);
|
||||
|
||||
return { isEnabled };
|
||||
};
|
||||
+5
-5
@@ -17,8 +17,8 @@ import { computed } from 'vue';
|
||||
|
||||
const WORKFLOW_ID = 'test-workflow-id';
|
||||
|
||||
vi.mock('@/features/resolvers/composables/useDynamicCredentials', () => ({
|
||||
useDynamicCredentials: vi.fn(),
|
||||
vi.mock('@/features/resolvers/composables/usePrivateCredentials', () => ({
|
||||
usePrivateCredentials: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/workflows/canvas/canvas.utils', async (importOriginal) => ({
|
||||
@@ -33,9 +33,9 @@ vi.mock('@/features/workflows/canvas/canvas.utils', async (importOriginal) => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
|
||||
const mockedUseDynamicCredentials = vi.mocked(useDynamicCredentials);
|
||||
const mockedUseDynamicCredentials = vi.mocked(usePrivateCredentials);
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeSettingsIcons, {
|
||||
pinia: createTestingPinia(),
|
||||
@@ -44,7 +44,7 @@ const renderComponent = createComponentRenderer(CanvasNodeSettingsIcons, {
|
||||
const mockFeatureFlag = (enabled: boolean) => {
|
||||
mockedUseDynamicCredentials.mockReturnValue({
|
||||
isEnabled: computed(() => enabled),
|
||||
} as ReturnType<typeof useDynamicCredentials>);
|
||||
} as ReturnType<typeof usePrivateCredentials>);
|
||||
};
|
||||
|
||||
describe('CanvasNodeSettingsIcons', () => {
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ import { computed } from 'vue';
|
||||
import { useCanvasNode } from '../../../../../composables/useCanvasNode';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
|
||||
|
||||
import { N8nIcon, N8nTooltip } from '@n8n/design-system';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
@@ -11,7 +11,7 @@ const { name } = useCanvasNode();
|
||||
const i18n = useI18n();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
|
||||
|
||||
const node = computed(() => workflowDocumentStore.value.getNodeByName(name.value));
|
||||
const size = 'small';
|
||||
@@ -117,7 +117,7 @@ const hasDynamicCredentials = computed(
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
|
||||
<N8nTooltip v-if="isDynamicCredentialsEnabled && hasDynamicCredentials">
|
||||
<N8nTooltip v-if="isPrivateCredentialsEnabled && hasDynamicCredentials">
|
||||
<template #content>
|
||||
<div :class="$style.tooltipHeader">
|
||||
<N8nIcon icon="key-round" :size="size" />
|
||||
|
||||
Reference in New Issue
Block a user