feat(editor): Add tracking on oidc save setting (#16378)

This commit is contained in:
Guillaume Jacquart
2025-06-16 15:10:37 +02:00
committed by GitHub
parent 58a556430c
commit 62a33e8074
2 changed files with 106 additions and 9 deletions

View File

@@ -1,4 +1,4 @@
import type { SamlPreferences } from '@n8n/api-types';
import type { OidcConfigDto, SamlPreferences } from '@n8n/api-types';
import { createTestingPinia } from '@pinia/testing';
import { within, waitFor } from '@testing-library/vue';
import { mockedStore, retry } from '@/__tests__/utils';
@@ -23,6 +23,10 @@ const samlConfig = {
returnUrl: 'https://n8n-tunnel.myhost.com/rest/sso/saml/acs',
} as SamlPreferences & SamlPreferencesExtractedData;
const oidcConfig = {
discoveryEndpoint: 'https://dev-qqkrykgkoo0p63d5.eu.auth0.com/.well-known/openid-configuration',
} as OidcConfigDto;
const telemetryTrack = vi.fn();
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: () => ({
@@ -152,7 +156,7 @@ describe('SettingsSso View', () => {
expect(telemetryTrack).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ identity_provider: 'metadata' }),
expect.objectContaining({ identity_provider: 'metadata', authentication_method: 'saml' }),
);
expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2);
@@ -282,6 +286,68 @@ describe('SettingsSso View', () => {
expect(toggle.textContent).toContain('Deactivated');
});
});
it('allows user to save OIDC config', async () => {
const pinia = createTestingPinia();
const ssoStore = mockedStore(useSSOStore);
ssoStore.saveOidcConfig.mockResolvedValue(oidcConfig);
ssoStore.isEnterpriseOidcEnabled = true;
ssoStore.isEnterpriseSamlEnabled = false;
ssoStore.isOidcLoginEnabled = true;
ssoStore.isSamlLoginEnabled = false;
const { getByTestId, getByRole } = renderView({ pinia });
// Set authProtocol component ref to OIDC
const protocolSelect = getByRole('combobox');
expect(protocolSelect).toBeInTheDocument();
await userEvent.click(protocolSelect);
const dropdown = await waitFor(() => getByRole('listbox'));
expect(dropdown).toBeInTheDocument();
const items = dropdown.querySelectorAll('.el-select-dropdown__item');
const oidcItem = Array.from(items).find((item) => item.textContent?.includes('OIDC'));
expect(oidcItem).toBeDefined();
await userEvent.click(oidcItem!);
const saveButton = await waitFor(() => getByTestId('sso-oidc-save'));
expect(saveButton).toBeVisible();
const oidcDiscoveryUrlInput = getByTestId('oidc-discovery-endpoint');
expect(oidcDiscoveryUrlInput).toBeVisible();
await userEvent.type(oidcDiscoveryUrlInput, oidcConfig.discoveryEndpoint);
const clientIdInput = getByTestId('oidc-client-id');
expect(clientIdInput).toBeVisible();
await userEvent.type(clientIdInput, 'test-client-id');
const clientSecretInput = getByTestId('oidc-client-secret');
expect(clientSecretInput).toBeVisible();
await userEvent.type(clientSecretInput, 'test-client-secret');
expect(saveButton).not.toBeDisabled();
await userEvent.click(saveButton);
expect(ssoStore.saveOidcConfig).toHaveBeenCalledWith(
expect.objectContaining({
discoveryEndpoint: oidcConfig.discoveryEndpoint,
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
loginEnabled: true,
}),
);
expect(telemetryTrack).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
authentication_method: 'oidc',
discovery_endpoint: oidcConfig.discoveryEndpoint,
is_active: true,
}),
);
});
});
let pinia: ReturnType<typeof createPinia>;

View File

@@ -118,6 +118,28 @@ const getSamlConfig = async () => {
ssoSettingsSaved.value = !!config?.metadata;
};
const trackUpdateSettings = () => {
const trackingMetadata: {
instance_id: string;
authentication_method: SupportedProtocolType;
is_active?: boolean;
discovery_endpoint?: string;
identity_provider?: 'metadata' | 'xml';
} = {
instance_id: rootStore.instanceId,
authentication_method: authProtocol.value,
};
if (authProtocol.value === SupportedProtocols.SAML) {
trackingMetadata.identity_provider = ipsType.value === 'url' ? 'metadata' : 'xml';
trackingMetadata.is_active = ssoStore.isSamlLoginEnabled;
} else if (authProtocol.value === SupportedProtocols.OIDC) {
trackingMetadata.discovery_endpoint = discoveryEndpoint.value;
trackingMetadata.is_active = ssoStore.isOidcLoginEnabled;
}
telemetry.track('User updated single sign on settings', trackingMetadata);
};
const onSave = async () => {
try {
validateInput();
@@ -142,11 +164,7 @@ const onSave = async () => {
}
}
telemetry.track('User updated single sign on settings', {
instance_id: rootStore.instanceId,
identity_provider: ipsType.value === 'url' ? 'metadata' : 'xml',
is_active: ssoStore.isSamlLoginEnabled,
});
trackUpdateSettings();
} catch (error) {
toast.showError(error, i18n.baseText('settings.sso.settings.save.error'));
return;
@@ -267,6 +285,7 @@ async function onOidcSettingsSave() {
});
clientSecret.value = newConfig.clientSecret;
trackUpdateSettings();
}
</script>
@@ -281,13 +300,17 @@ async function onOidcSettingsSave() {
{{ i18n.baseText('settings.sso.info.link') }}
</a>
</n8n-info-tip>
<div v-if="ssoStore.isEnterpriseSamlEnabled" data-test-id="sso-content-licensed">
<div
v-if="ssoStore.isEnterpriseSamlEnabled || ssoStore.isEnterpriseOidcEnabled"
data-test-id="sso-content-licensed"
>
<div :class="$style.group">
<label>Select Authentication Protocol</label>
<div>
<N8nSelect
filterable
:model-value="authProtocol"
data-test-id="sso-auth-protocol-select"
:placeholder="i18n.baseText('parameterInput.select')"
@update:model-value="onAuthProtocolUpdated"
@keydown.stop
@@ -407,6 +430,7 @@ async function onOidcSettingsSave() {
<N8nInput
:model-value="discoveryEndpoint"
type="text"
data-test-id="oidc-discovery-endpoint"
placeholder="https://accounts.google.com/.well-known/openid-configuration"
@update:model-value="(v: string) => (discoveryEndpoint = v)"
/>
@@ -417,6 +441,7 @@ async function onOidcSettingsSave() {
<N8nInput
:model-value="clientId"
type="text"
data-test-id="oidc-client-id"
@update:model-value="(v: string) => (clientId = v)"
/>
<small
@@ -428,6 +453,7 @@ async function onOidcSettingsSave() {
<N8nInput
:model-value="clientSecret"
type="password"
data-test-id="oidc-client-secret"
@update:model-value="(v: string) => (clientSecret = v)"
/>
<small
@@ -445,7 +471,12 @@ async function onOidcSettingsSave() {
</div>
<div :class="$style.buttons">
<n8n-button size="large" :disabled="cannotSaveOidcSettings" @click="onOidcSettingsSave">
<n8n-button
data-test-id="sso-oidc-save"
size="large"
:disabled="cannotSaveOidcSettings"
@click="onOidcSettingsSave"
>
{{ i18n.baseText('settings.sso.settings.save') }}
</n8n-button>
</div>