Skip to content

Commit 6786915

Browse files
feat(core): Make user role provisioning available on enterprise (#22166)
1 parent 60a9cbf commit 6786915

File tree

9 files changed

+62
-81
lines changed

9 files changed

+62
-81
lines changed

packages/@n8n/backend-common/src/license-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export class LicenseState {
175175
}
176176

177177
isProvisioningLicensed() {
178-
return this.isLicensed(['feat:saml', 'feat:oidc', 'feat:ldap']);
178+
return this.isLicensed(['feat:saml', 'feat:oidc']);
179179
}
180180

181181
// --------------------

packages/frontend/editor-ui/src/app/constants/experiments.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,6 @@ export const PERSONALIZED_TEMPLATES_V3 = {
8383
variant: 'variant',
8484
};
8585

86-
export const SSO_JUST_IN_TIME_PROVSIONING_EXPERIMENT = {
87-
name: '050_sso_jit_provisioning',
88-
};
89-
9086
export const EXPERIMENTS_TO_TRACK = [
9187
EXTRA_TEMPLATE_LINKS_EXPERIMENT.name,
9288
TEMPLATE_ONBOARDING_EXPERIMENT.name,
@@ -96,5 +92,4 @@ export const EXPERIMENTS_TO_TRACK = [
9692
TEMPLATE_RECO_V2.name,
9793
TEMPLATES_DATA_QUALITY_EXPERIMENT.name,
9894
READY_TO_RUN_V2_PART2_EXPERIMENT.name,
99-
SSO_JUST_IN_TIME_PROVSIONING_EXPERIMENT.name,
10095
];

packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import { SupportedProtocols, useSSOStore } from '../sso.store';
55
import { useI18n } from '@n8n/i18n';
66
77
import { ElCheckbox } from 'element-plus';
8-
import { N8nActionBox, N8nButton, N8nInput, N8nOption, N8nSelect } from '@n8n/design-system';
8+
import { N8nButton, N8nInput, N8nOption, N8nSelect } from '@n8n/design-system';
99
import { computed, onMounted, ref } from 'vue';
1010
import { useToast } from '@/app/composables/useToast';
11-
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
1211
import { useMessage } from '@/app/composables/useMessage';
1312
import UserRoleProvisioningDropdown, {
1413
type UserRoleProvisioningSetting,
@@ -24,7 +23,6 @@ const ssoStore = useSSOStore();
2423
const telemetry = useTelemetry();
2524
const toast = useToast();
2625
const message = useMessage();
27-
const pageRedirectionHelper = usePageRedirectionHelper();
2826
const instanceId = useRootStore().instanceId;
2927
3028
const savingForm = ref<boolean>(false);
@@ -189,16 +187,12 @@ function sendTrackingEventForUserProvisioning() {
189187
});
190188
}
191189
192-
const goToUpgrade = () => {
193-
void pageRedirectionHelper.goToUpgrade('sso', 'upgrade-sso');
194-
};
195-
196190
onMounted(async () => {
197191
await loadOidcConfig();
198192
});
199193
</script>
200194
<template>
201-
<div v-if="ssoStore.isEnterpriseOidcEnabled">
195+
<div>
202196
<div :class="$style.group">
203197
<label>Redirect URL</label>
204198
<CopyInput
@@ -298,16 +292,5 @@ onMounted(async () => {
298292
</N8nButton>
299293
</div>
300294
</div>
301-
<N8nActionBox
302-
v-else
303-
data-test-id="sso-content-unlicensed"
304-
:class="$style.actionBox"
305-
:button-text="i18n.baseText('settings.sso.actionBox.buttonText')"
306-
@click:button="goToUpgrade"
307-
>
308-
<template #heading>
309-
<span>{{ i18n.baseText('settings.sso.actionBox.title') }}</span>
310-
</template>
311-
</N8nActionBox>
312295
</template>
313296
<style lang="scss" module src="../styles/sso-form.module.scss" />

packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { useI18n } from '@n8n/i18n';
66
import { captureMessage } from '@sentry/vue';
77
88
import { ElCheckbox } from 'element-plus';
9-
import { N8nActionBox, N8nButton, N8nInput, N8nRadioButtons } from '@n8n/design-system';
9+
import { N8nButton, N8nInput, N8nRadioButtons } from '@n8n/design-system';
1010
import { useToast } from '@/app/composables/useToast';
11-
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
1211
import { useMessage } from '@/app/composables/useMessage';
1312
import { computed, onMounted, ref } from 'vue';
1413
import UserRoleProvisioningDropdown, {
@@ -25,7 +24,6 @@ const ssoStore = useSSOStore();
2524
const telemetry = useTelemetry();
2625
const toast = useToast();
2726
const message = useMessage();
28-
const pageRedirectionHelper = usePageRedirectionHelper();
2927
const instanceId = useRootStore().instanceId;
3028
3129
const savingForm = ref<boolean>(false);
@@ -278,16 +276,12 @@ const validateSamlInput = () => {
278276
}
279277
};
280278
281-
const goToUpgrade = () => {
282-
void pageRedirectionHelper.goToUpgrade('sso', 'upgrade-sso');
283-
};
284-
285279
onMounted(async () => {
286280
await loadSamlConfig();
287281
});
288282
</script>
289283
<template>
290-
<div v-if="ssoStore.isEnterpriseSamlEnabled" data-test-id="sso-content-licensed">
284+
<div>
291285
<div :class="$style.group">
292286
<label>{{ i18n.baseText('settings.sso.settings.redirectUrl.label') }}</label>
293287
<CopyInput
@@ -367,18 +361,6 @@ onMounted(async () => {
367361
</N8nButton>
368362
</div>
369363
</div>
370-
<N8nActionBox
371-
v-else
372-
data-test-id="sso-content-unlicensed"
373-
:class="$style.actionBox"
374-
:description="i18n.baseText('settings.sso.actionBox.description')"
375-
:button-text="i18n.baseText('settings.sso.actionBox.buttonText')"
376-
@click:button="goToUpgrade"
377-
>
378-
<template #heading>
379-
<span>{{ i18n.baseText('settings.sso.actionBox.title') }}</span>
380-
</template>
381-
</N8nActionBox>
382364
</template>
383365

384366
<style lang="scss" module src="../styles/sso-form.module.scss" />

packages/frontend/editor-ui/src/features/settings/sso/provisioning/components/UserRoleProvisioningDropdown.vue

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
<script lang="ts" setup>
2-
import { SSO_JUST_IN_TIME_PROVSIONING_EXPERIMENT } from '@/app/constants';
32
import type { ProvisioningConfig } from '@n8n/rest-api-client/api/provisioning';
43
54
import { N8nOption, N8nSelect } from '@n8n/design-system';
65
import { onMounted } from 'vue';
7-
import { usePostHog } from '@/app/stores/posthog.store';
86
import { useUserRoleProvisioningStore } from '../composables/userRoleProvisioning.store';
97
import { useI18n } from '@n8n/i18n';
108
import { type SupportedProtocolType } from '../../sso.store';
9+
import { useRBACStore } from '@/app/stores/rbac.store';
1110
1211
export type UserRoleProvisioningSetting =
1312
| 'disabled'
@@ -21,12 +20,8 @@ const { authProtocol } = defineProps<{
2120
}>();
2221
2322
const i18n = useI18n();
24-
const posthogStore = usePostHog();
2523
const userRoleProvisioningStore = useUserRoleProvisioningStore();
26-
27-
const isUserRoleProvisioningFeatureEnabled = posthogStore.isFeatureEnabled(
28-
SSO_JUST_IN_TIME_PROVSIONING_EXPERIMENT.name,
29-
);
24+
const canManageUserProvisioning = useRBACStore().hasScope('provisioning:manage');
3025
3126
const handleUserRoleProvisioningChange = (newValue: UserRoleProvisioningSetting) => {
3227
value.value = newValue;
@@ -79,11 +74,11 @@ onMounted(async () => {
7974
});
8075
</script>
8176
<template>
82-
<!-- TODO: also check for 'provisioning:manage' permission scope -->
83-
<div v-if="isUserRoleProvisioningFeatureEnabled" :class="$style.group">
77+
<div :class="$style.group">
8478
<label>{{ i18n.baseText('settings.sso.settings.userRoleProvisioning.label') }}</label>
8579
<N8nSelect
8680
:model-value="value"
81+
:disabled="!canManageUserProvisioning"
8782
data-test-id="oidc-user-role-provisioning"
8883
:class="$style.userRoleProvisioningSelect"
8984
@update:model-value="handleUserRoleProvisioningChange"

packages/frontend/editor-ui/src/features/settings/sso/provisioning/composables/useAccessSettingsCsvExport.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ export function useAccessSettingsCsvExport() {
104104
for (const project of user.projectRelations) {
105105
const projectName = escapeCsvValue(project.name ?? '');
106106
const projectId = escapeCsvValue(project.id ?? '');
107-
const projectRole = escapeCsvValue(project.role ?? '');
107+
// In our backend, project roles are stored like this: project:viewer
108+
// But to make the configuration of project role provisioning easier,
109+
// we omit this part of the role value in the SSO config.
110+
const roleValueForProvisioning = project.role.split(':')[1] ?? project.role;
111+
const projectRole = escapeCsvValue(roleValueForProvisioning);
108112
csvRows.push(`${email},${projectName},${projectId},${projectRole}`);
109113
}
110114
}

packages/frontend/editor-ui/src/features/settings/sso/provisioning/composables/useUserRoleProvisioningForm.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { Ref } from 'vue';
22
import { useUserRoleProvisioningStore } from './userRoleProvisioning.store';
3-
import { usePostHog } from '@/app/stores/posthog.store';
4-
import { SSO_JUST_IN_TIME_PROVSIONING_EXPERIMENT } from '@/app/constants/experiments';
53
import type { ProvisioningConfig } from '@n8n/rest-api-client/api/provisioning';
64
import { type UserRoleProvisioningSetting } from '../components/UserRoleProvisioningDropdown.vue';
75

@@ -12,7 +10,6 @@ export function useUserRoleProvisioningForm(
1210
userRoleProvisioning: Ref<UserRoleProvisioningSetting>,
1311
) {
1412
const provisioningStore = useUserRoleProvisioningStore();
15-
const posthogStore = usePostHog();
1613

1714
const getUserRoleProvisioningValueFromConfig = (
1815
config?: ProvisioningConfig,
@@ -51,9 +48,6 @@ export function useUserRoleProvisioningForm(
5148
};
5249

5350
const isUserRoleProvisioningChanged = (): boolean => {
54-
if (!posthogStore.isFeatureEnabled(SSO_JUST_IN_TIME_PROVSIONING_EXPERIMENT.name)) {
55-
return false;
56-
}
5751
return (
5852
getUserRoleProvisioningValueFromConfig(provisioningStore.provisioningConfig) !==
5953
userRoleProvisioning.value

packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -392,10 +392,10 @@ describe('SettingsSso View', () => {
392392
});
393393
ssoStore.saveOidcConfig.mockResolvedValue({ ...oidcConfig, loginEnabled: true });
394394

395-
const { getByTestId, getByRole } = renderView();
395+
const { getByTestId, getByRole, getAllByRole } = renderView();
396396

397397
// Set authProtocol component ref to OIDC
398-
const protocolSelect = getByRole('combobox');
398+
const protocolSelect = getAllByRole('combobox')[0];
399399
expect(protocolSelect).toBeInTheDocument();
400400
await userEvent.click(protocolSelect);
401401

@@ -463,11 +463,11 @@ describe('SettingsSso View', () => {
463463
discoveryEndpoint: '',
464464
});
465465

466-
const { getByTestId, getByRole } = renderView();
466+
const { getByTestId, getByRole, getAllByRole } = renderView();
467467
showError.mockClear();
468468

469469
// Set authProtocol component ref to OIDC
470-
const protocolSelect = getByRole('combobox');
470+
const protocolSelect = getAllByRole('combobox')[0];
471471
expect(protocolSelect).toBeInTheDocument();
472472
await userEvent.click(protocolSelect);
473473

@@ -519,10 +519,10 @@ describe('SettingsSso View', () => {
519519
ssoStore.getSamlConfig.mockResolvedValue(samlConfig);
520520
ssoStore.getOidcConfig.mockResolvedValue(oidcConfig);
521521

522-
const { getByRole } = renderView();
522+
const { getByRole, getAllByRole } = renderView();
523523

524524
// Change protocol selection in dropdown
525-
const protocolSelect = getByRole('combobox');
525+
const protocolSelect = getAllByRole('combobox')[0];
526526
await userEvent.click(protocolSelect);
527527

528528
const dropdown = await waitFor(() => getByRole('listbox'));
@@ -542,10 +542,10 @@ describe('SettingsSso View', () => {
542542
ssoStore.getSamlConfig.mockResolvedValue(samlConfig);
543543
ssoStore.getOidcConfig.mockResolvedValue(oidcConfig);
544544

545-
const { getByRole, getByTestId } = renderView();
545+
const { getByRole, getByTestId, getAllByRole } = renderView();
546546

547547
// Change to SAML protocol in dropdown
548-
const protocolSelect = getByRole('combobox');
548+
const protocolSelect = getAllByRole('combobox')[0];
549549
await userEvent.click(protocolSelect);
550550

551551
const dropdown = await waitFor(() => getByRole('listbox'));
@@ -578,10 +578,10 @@ describe('SettingsSso View', () => {
578578
ssoStore.getOidcConfig.mockResolvedValue(oidcConfig);
579579
ssoStore.saveOidcConfig.mockResolvedValue(oidcConfig);
580580

581-
const { getByRole, getByTestId } = renderView();
581+
const { getByRole, getByTestId, getAllByRole } = renderView();
582582

583583
// Change to OIDC protocol in dropdown
584-
const protocolSelect = getByRole('combobox');
584+
const protocolSelect = getAllByRole('combobox')[0];
585585
await userEvent.click(protocolSelect);
586586

587587
const dropdown = await waitFor(() => getByRole('listbox'));
@@ -641,14 +641,14 @@ describe('SettingsSso View', () => {
641641
ssoStore.getSamlConfig.mockResolvedValue(samlConfig);
642642
ssoStore.getOidcConfig.mockResolvedValue(oidcConfig);
643643

644-
const { getByRole, getByTestId, queryByTestId } = renderView();
644+
const { getByRole, getAllByRole, getByTestId, queryByTestId } = renderView();
645645

646646
// Initially should show SAML content (matching store)
647647
expect(getByTestId('sso-provider-url')).toBeVisible();
648648
expect(queryByTestId('oidc-discovery-endpoint')).not.toBeInTheDocument();
649649

650650
// Change to OIDC in dropdown (local state only)
651-
const protocolSelect = getByRole('combobox');
651+
const protocolSelect = getAllByRole('combobox')[0];
652652
await userEvent.click(protocolSelect);
653653

654654
const dropdown = await waitFor(() => getByRole('listbox'));

packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.vue

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
<script lang="ts" setup>
22
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
3+
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
34
import { useSSOStore, SupportedProtocols, type SupportedProtocolType } from '../sso.store';
45
import { useI18n } from '@n8n/i18n';
56
import { computed, onMounted, ref } from 'vue';
67
7-
import { N8nHeading, N8nInfoTip, N8nOption, N8nSelect } from '@n8n/design-system';
8+
import { N8nActionBox, N8nHeading, N8nInfoTip, N8nOption, N8nSelect } from '@n8n/design-system';
89
import SamlSettingsForm from '../components/SamlSettingsForm.vue';
910
import OidcSettingsForm from '../components/OidcSettingsForm.vue';
1011
1112
const i18n = useI18n();
1213
const ssoStore = useSSOStore();
1314
const documentTitle = useDocumentTitle();
15+
const pageRedirectionHelper = usePageRedirectionHelper();
1416
1517
const options = computed(() => {
1618
return [
@@ -27,11 +29,19 @@ const options = computed(() => {
2729
];
2830
});
2931
32+
const hasAnySsoEnabled = computed(
33+
() => ssoStore.isEnterpriseSamlEnabled || ssoStore.isEnterpriseOidcEnabled,
34+
);
35+
3036
const authProtocol = ref<SupportedProtocolType>(SupportedProtocols.SAML);
3137
function onAuthProtocolUpdated(value: SupportedProtocolType) {
3238
authProtocol.value = value;
3339
}
3440
41+
const goToUpgrade = () => {
42+
void pageRedirectionHelper.goToUpgrade('sso', 'upgrade-sso');
43+
};
44+
3545
onMounted(() => {
3646
documentTitle.set(i18n.baseText('settings.sso.title'));
3747
ssoStore.initializeSelectedProtocol();
@@ -50,11 +60,7 @@ onMounted(() => {
5060
{{ i18n.baseText('settings.sso.info.link') }}
5161
</a>
5262
</N8nInfoTip>
53-
<div
54-
v-if="ssoStore.isEnterpriseSamlEnabled || ssoStore.isEnterpriseOidcEnabled"
55-
data-test-id="sso-auth-protocol-select"
56-
:class="shared.group"
57-
>
63+
<div v-if="hasAnySsoEnabled" data-test-id="sso-auth-protocol-select" :class="shared.group">
5864
<label>Select Authentication Protocol</label>
5965
<div>
6066
<N8nSelect
@@ -75,12 +81,30 @@ onMounted(() => {
7581
</N8nSelect>
7682
</div>
7783
</div>
78-
<div v-if="authProtocol === SupportedProtocols.SAML">
84+
<div
85+
v-if="ssoStore.isEnterpriseSamlEnabled && authProtocol === SupportedProtocols.SAML"
86+
data-test-id="sso-content-licensed"
87+
>
7988
<SamlSettingsForm />
8089
</div>
81-
<div v-if="authProtocol === SupportedProtocols.OIDC">
90+
<div
91+
v-if="ssoStore.isEnterpriseOidcEnabled && authProtocol === SupportedProtocols.OIDC"
92+
data-test-id="sso-content-licensed"
93+
>
8294
<OidcSettingsForm />
8395
</div>
96+
<N8nActionBox
97+
v-if="!hasAnySsoEnabled"
98+
data-test-id="sso-content-unlicensed"
99+
:class="$style.actionBox"
100+
:description="i18n.baseText('settings.sso.actionBox.description')"
101+
:button-text="i18n.baseText('settings.sso.actionBox.buttonText')"
102+
@click:button="goToUpgrade"
103+
>
104+
<template #heading>
105+
<span>{{ i18n.baseText('settings.sso.actionBox.title') }}</span>
106+
</template>
107+
</N8nActionBox>
84108
</div>
85109
</template>
86110

@@ -90,4 +114,8 @@ onMounted(() => {
90114
.heading {
91115
margin-bottom: var(--spacing--sm);
92116
}
117+
118+
.actionBox {
119+
margin-top: var(--spacing--lg);
120+
}
93121
</style>

0 commit comments

Comments
 (0)