mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
chore(editor): Gate personal space policy settings behind license feature (#25527)
This commit is contained in:
parent
88017640c4
commit
cfca041d0e
|
|
@ -60,6 +60,7 @@ export interface IEnterpriseSettings {
|
|||
};
|
||||
};
|
||||
customRoles: boolean;
|
||||
personalSpacePolicy: boolean;
|
||||
}
|
||||
|
||||
export interface FrontendSettings {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ export class LicenseState {
|
|||
return this.isLicensed(LICENSE_FEATURES.DYNAMIC_CREDENTIALS);
|
||||
}
|
||||
|
||||
isPersonalSpacePolicyLicensed() {
|
||||
return this.isLicensed(LICENSE_FEATURES.PERSONAL_SPACE_POLICY);
|
||||
}
|
||||
|
||||
isSharingLicensed() {
|
||||
return this.isLicensed('feat:sharing');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export const LICENSE_FEATURES = {
|
|||
CUSTOM_ROLES: 'feat:customRoles',
|
||||
AI_BUILDER: 'feat:aiBuilder',
|
||||
DYNAMIC_CREDENTIALS: 'feat:dynamicCredentials',
|
||||
PERSONAL_SPACE_POLICY: 'feat:personalSpacePolicy',
|
||||
} as const;
|
||||
|
||||
export const LICENSE_QUOTAS = {
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export class E2EController {
|
|||
[LICENSE_FEATURES.NAMED_VERSIONS]: false,
|
||||
[LICENSE_FEATURES.CUSTOM_ROLES]: false,
|
||||
[LICENSE_FEATURES.AI_BUILDER]: false,
|
||||
[LICENSE_FEATURES.PERSONAL_SPACE_POLICY]: false,
|
||||
};
|
||||
|
||||
private static readonly numericFeaturesDefaults: Record<NumericLicenseFeature, number> = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { UpdateSecuritySettingsDto } from '@n8n/api-types';
|
||||
import { type AuthenticatedRequest } from '@n8n/db';
|
||||
import { Body, Get, GlobalScope, Post, RestController } from '@n8n/decorators';
|
||||
import { Body, Get, GlobalScope, Licensed, Post, RestController } from '@n8n/decorators';
|
||||
import {
|
||||
PERSONAL_SPACE_PUBLISHING_SETTING,
|
||||
PERSONAL_SPACE_SHARING_SETTING,
|
||||
|
|
@ -17,6 +17,7 @@ export class SecuritySettingsController {
|
|||
private readonly eventService: EventService,
|
||||
) {}
|
||||
|
||||
@Licensed('feat:personalSpacePolicy')
|
||||
@GlobalScope('securitySettings:manage')
|
||||
@Get('/')
|
||||
async getSecuritySettings(_req: AuthenticatedRequest, _res: Response) {
|
||||
|
|
@ -39,6 +40,7 @@ export class SecuritySettingsController {
|
|||
};
|
||||
}
|
||||
|
||||
@Licensed('feat:personalSpacePolicy')
|
||||
@GlobalScope('securitySettings:manage')
|
||||
@Post('/')
|
||||
async updateSecuritySettings(
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@ export class FrontendService {
|
|||
},
|
||||
},
|
||||
customRoles: false,
|
||||
personalSpacePolicy: false,
|
||||
},
|
||||
mfa: {
|
||||
enabled: false,
|
||||
|
|
@ -460,6 +461,7 @@ export class FrontendService {
|
|||
workflowDiffs: this.licenseState.isWorkflowDiffsLicensed(),
|
||||
namedVersions: this.license.isLicensed(LICENSE_FEATURES.NAMED_VERSIONS),
|
||||
customRoles: this.licenseState.isCustomRolesLicensed(),
|
||||
personalSpacePolicy: this.licenseState.isPersonalSpacePolicyLicensed(),
|
||||
});
|
||||
|
||||
if (this.license.isLdapEnabled()) {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,15 @@ describe('SecuritySettingsController', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
testServer.license.enable('feat:personalSpacePolicy');
|
||||
});
|
||||
|
||||
describe('GET /settings/security', () => {
|
||||
it('should return 403 when personalSpacePolicy license is not active', async () => {
|
||||
testServer.license.disable('feat:personalSpacePolicy');
|
||||
await ownerAgent.get('/settings/security').expect(403);
|
||||
});
|
||||
|
||||
it('should return security settings and all counts', async () => {
|
||||
securitySettingsService.arePersonalSpaceSettingsEnabled.mockResolvedValue({
|
||||
personalSpacePublishing: true,
|
||||
|
|
@ -78,6 +84,14 @@ describe('SecuritySettingsController', () => {
|
|||
});
|
||||
|
||||
describe('POST /settings/security', () => {
|
||||
it('should return 403 when personalSpacePolicy license is not active', async () => {
|
||||
testServer.license.disable('feat:personalSpacePolicy');
|
||||
await ownerAgent
|
||||
.post('/settings/security')
|
||||
.send({ personalSpacePublishing: true })
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should update only personalSpacePublishing when only that is set in body', async () => {
|
||||
securitySettingsService.setPersonalSpaceSetting.mockResolvedValue(undefined);
|
||||
|
||||
|
|
|
|||
|
|
@ -3995,6 +3995,8 @@
|
|||
"settings.security.personalSpace.sharing.confirmMessage.disable.message": "This will prevent sharing workflows and credentials from personal spaces going forwards. Existing shares will remain in place.",
|
||||
"settings.security.personalSpace.sharing.existingCount.label": "Existing shares",
|
||||
"settings.security.personalSpace.sharing.existingCount.value": "{workflowCount} workflows, {credentialCount} credentials",
|
||||
"settings.security.personalSpace.unlicensed_tooltip": "Upgrade to a plan that includes personal space policies to manage this setting. {action}",
|
||||
"settings.security.personalSpace.unlicensed_tooltip.link": "Upgrade now",
|
||||
"settings.sso": "SSO",
|
||||
"settings.sso.title": "Single Sign On",
|
||||
"settings.sso.subtitle": "SAML 2.0 Configuration",
|
||||
|
|
|
|||
|
|
@ -949,6 +949,7 @@ export type EnterpriseEditionFeatureKey =
|
|||
| 'EnforceMFA'
|
||||
| 'NamedVersions'
|
||||
| 'Provisioning'
|
||||
| 'PersonalSpacePolicy'
|
||||
| 'CustomRoles';
|
||||
|
||||
export type EnterpriseEditionFeatureValue = keyof Omit<FrontendSettings['enterprise'], 'projects'>;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const defaultSettings: FrontendSettings = {
|
|||
},
|
||||
},
|
||||
customRoles: false,
|
||||
personalSpacePolicy: false,
|
||||
},
|
||||
executionMode: 'regular',
|
||||
isMultiMain: false,
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ export function createMockEnterpriseSettings(
|
|||
},
|
||||
},
|
||||
customRoles: false,
|
||||
personalSpacePolicy: false,
|
||||
...overrides, // Override with any passed properties
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,6 @@ export const EnterpriseEditionFeature: Record<
|
|||
ApiKeyScopes: 'apiKeyScopes',
|
||||
NamedVersions: 'namedVersions',
|
||||
Provisioning: 'provisioning',
|
||||
PersonalSpacePolicy: 'personalSpacePolicy',
|
||||
CustomRoles: 'customRoles',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ describe('CredentialSharing.ee', () => {
|
|||
},
|
||||
},
|
||||
customRoles: false,
|
||||
personalSpacePolicy: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -279,6 +280,7 @@ describe('CredentialSharing.ee', () => {
|
|||
},
|
||||
},
|
||||
customRoles: false,
|
||||
personalSpacePolicy: false,
|
||||
});
|
||||
|
||||
const credential = createCredential();
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ describe('SecuritySettings', () => {
|
|||
|
||||
settingsStore.isMFAEnforced = false;
|
||||
settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.EnforceMFA] = true;
|
||||
settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.PersonalSpacePolicy] = true;
|
||||
usersStore.updateEnforceMfa = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
// Enable PERSONAL_SECURITY_SETTINGS env feature flag for Personal Space section
|
||||
|
|
@ -361,4 +362,58 @@ describe('SecuritySettings', () => {
|
|||
expect(showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when personalSpacePolicy feature is not licensed', () => {
|
||||
beforeEach(() => {
|
||||
settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.PersonalSpacePolicy] =
|
||||
false;
|
||||
});
|
||||
|
||||
it('should show upgrade badges on sharing and publishing titles', async () => {
|
||||
const { getAllByText, getByTestId } = renderView();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('security-personal-space-sharing-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Two upgrade badges for sharing + publishing (plus one for MFA if unlicensed)
|
||||
const upgradeBadges = getAllByText('Upgrade');
|
||||
expect(upgradeBadges.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should render disabled sharing toggle when unlicensed', async () => {
|
||||
const { getByTestId } = renderView();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('security-personal-space-sharing-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const sharingToggle = getByTestId('security-personal-space-sharing-toggle');
|
||||
expect(sharingToggle).toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should render disabled publishing toggle when unlicensed', async () => {
|
||||
const { getByTestId } = renderView();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('security-personal-space-publishing-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const publishingToggle = getByTestId('security-personal-space-publishing-toggle');
|
||||
expect(publishingToggle).toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should not call updateSecuritySettings when clicking disabled toggle', async () => {
|
||||
const { getByTestId } = renderView();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('security-personal-space-sharing-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const sharingToggle = getByTestId('security-personal-space-sharing-toggle');
|
||||
await userEvent.click(sharingToggle);
|
||||
|
||||
expect(updateSecuritySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,12 +25,17 @@ const { showToast, showError } = useToast();
|
|||
const message = useMessage();
|
||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||
|
||||
const tooltipKey = 'settings.personal.mfa.enforce.unlicensed_tooltip';
|
||||
const mfaTooltipKey = 'settings.personal.mfa.enforce.unlicensed_tooltip';
|
||||
const personalSpaceTooltipKey = 'settings.security.personalSpace.unlicensed_tooltip';
|
||||
|
||||
const isEnforceMFAEnabled = computed(
|
||||
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.EnforceMFA],
|
||||
);
|
||||
|
||||
const isPersonalSpacePolicyLicensed = computed(
|
||||
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.PersonalSpacePolicy],
|
||||
);
|
||||
|
||||
async function onUpdateMfaEnforced(value: string | number | boolean) {
|
||||
const boolValue = typeof value === 'boolean' ? value : Boolean(value);
|
||||
try {
|
||||
|
|
@ -188,7 +193,7 @@ const sharingCountText = computed(() => {
|
|||
:disabled="true"
|
||||
/>
|
||||
<template #content>
|
||||
<I18nT :keypath="tooltipKey" tag="span" scope="global">
|
||||
<I18nT :keypath="mfaTooltipKey" tag="span" scope="global">
|
||||
<template #action>
|
||||
<a @click="goToUpgrade">
|
||||
{{ i18n.baseText('settings.personal.mfa.enforce.unlicensed_tooltip.link') }}
|
||||
|
|
@ -211,20 +216,46 @@ const sharingCountText = computed(() => {
|
|||
<div :class="$style.settingsSection">
|
||||
<div :class="$style.settingsContainer">
|
||||
<div :class="$style.settingsContainerInfo">
|
||||
<N8nText :bold="true">
|
||||
{{ i18n.baseText('settings.security.personalSpace.sharing.title') }}
|
||||
<N8nText :bold="true"
|
||||
>{{ i18n.baseText('settings.security.personalSpace.sharing.title') }}
|
||||
<N8nBadge v-if="!isPersonalSpacePolicyLicensed" class="ml-4xs">{{
|
||||
i18n.baseText('generic.upgrade')
|
||||
}}</N8nBadge>
|
||||
</N8nText>
|
||||
<N8nText size="small" color="text-light">
|
||||
{{ i18n.baseText('settings.security.personalSpace.sharing.description') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
<div :class="$style.settingsContainerAction">
|
||||
<ElSwitch
|
||||
v-model="personalSpaceSharing"
|
||||
:loading="isLoading"
|
||||
size="large"
|
||||
data-test-id="security-personal-space-sharing-toggle"
|
||||
/>
|
||||
<EnterpriseEdition :features="[EnterpriseEditionFeature.PersonalSpacePolicy]">
|
||||
<ElSwitch
|
||||
v-model="personalSpaceSharing"
|
||||
:loading="isLoading"
|
||||
size="large"
|
||||
data-test-id="security-personal-space-sharing-toggle"
|
||||
/>
|
||||
<template #fallback>
|
||||
<N8nTooltip>
|
||||
<ElSwitch
|
||||
:model-value="false"
|
||||
size="large"
|
||||
:disabled="true"
|
||||
data-test-id="security-personal-space-sharing-toggle"
|
||||
/>
|
||||
<template #content>
|
||||
<I18nT :keypath="personalSpaceTooltipKey" tag="span" scope="global">
|
||||
<template #action>
|
||||
<a @click="goToUpgrade">
|
||||
{{
|
||||
i18n.baseText('settings.security.personalSpace.unlicensed_tooltip.link')
|
||||
}}
|
||||
</a>
|
||||
</template>
|
||||
</I18nT>
|
||||
</template>
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
</EnterpriseEdition>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.settingsCountRow" data-test-id="security-sharing-count">
|
||||
|
|
@ -240,20 +271,46 @@ const sharingCountText = computed(() => {
|
|||
<div :class="$style.settingsSection">
|
||||
<div :class="$style.settingsContainer">
|
||||
<div :class="$style.settingsContainerInfo">
|
||||
<N8nText :bold="true">
|
||||
{{ i18n.baseText('settings.security.personalSpace.publishing.title') }}
|
||||
<N8nText :bold="true"
|
||||
>{{ i18n.baseText('settings.security.personalSpace.publishing.title') }}
|
||||
<N8nBadge v-if="!isPersonalSpacePolicyLicensed" class="ml-4xs">{{
|
||||
i18n.baseText('generic.upgrade')
|
||||
}}</N8nBadge>
|
||||
</N8nText>
|
||||
<N8nText size="small" color="text-light">
|
||||
{{ i18n.baseText('settings.security.personalSpace.publishing.description') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
<div :class="$style.settingsContainerAction">
|
||||
<ElSwitch
|
||||
v-model="personalSpacePublishing"
|
||||
:loading="isLoading"
|
||||
size="large"
|
||||
data-test-id="security-personal-space-publishing-toggle"
|
||||
/>
|
||||
<EnterpriseEdition :features="[EnterpriseEditionFeature.PersonalSpacePolicy]">
|
||||
<ElSwitch
|
||||
v-model="personalSpacePublishing"
|
||||
:loading="isLoading"
|
||||
size="large"
|
||||
data-test-id="security-personal-space-publishing-toggle"
|
||||
/>
|
||||
<template #fallback>
|
||||
<N8nTooltip>
|
||||
<ElSwitch
|
||||
:model-value="false"
|
||||
size="large"
|
||||
:disabled="true"
|
||||
data-test-id="security-personal-space-publishing-toggle"
|
||||
/>
|
||||
<template #content>
|
||||
<I18nT :keypath="personalSpaceTooltipKey" tag="span" scope="global">
|
||||
<template #action>
|
||||
<a @click="goToUpgrade">
|
||||
{{
|
||||
i18n.baseText('settings.security.personalSpace.unlicensed_tooltip.link')
|
||||
}}
|
||||
</a>
|
||||
</template>
|
||||
</I18nT>
|
||||
</template>
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
</EnterpriseEdition>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.settingsCountRow" data-test-id="security-publishing-count">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user