diff --git a/packages/@n8n/permissions/src/constants.ts b/packages/@n8n/permissions/src/constants.ts index c43677e843..7a0ebf2cb1 100644 --- a/packages/@n8n/permissions/src/constants.ts +++ b/packages/@n8n/permissions/src/constants.ts @@ -3,6 +3,7 @@ export const RESOURCES = { annotationTag: [...DEFAULT_OPERATIONS] as const, auditLogs: ['manage'] as const, banner: ['dismiss'] as const, + community: ['register'] as const, communityPackage: ['install', 'uninstall', 'update', 'list', 'manage'] as const, credential: ['share', 'move', ...DEFAULT_OPERATIONS] as const, externalSecretsProvider: ['sync', ...DEFAULT_OPERATIONS] as const, diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index 1a78f79f15..07ed750f91 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -13,6 +13,7 @@ export type WildcardScope = `${Resource}:*` | '*'; export type AnnotationTagScope = ResourceScope<'annotationTag'>; export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>; export type BannerScope = ResourceScope<'banner', 'dismiss'>; +export type CommunityScope = ResourceScope<'community', 'register'>; export type CommunityPackageScope = ResourceScope< 'communityPackage', 'install' | 'uninstall' | 'update' | 'list' | 'manage' @@ -48,6 +49,7 @@ export type Scope = | AnnotationTagScope | AuditLogsScope | BannerScope + | CommunityScope | CommunityPackageScope | CredentialScope | ExternalSecretProviderScope diff --git a/packages/cli/src/permissions/global-roles.ts b/packages/cli/src/permissions/global-roles.ts index 6315c3c617..7ea1b575da 100644 --- a/packages/cli/src/permissions/global-roles.ts +++ b/packages/cli/src/permissions/global-roles.ts @@ -15,6 +15,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [ 'credential:list', 'credential:share', 'credential:move', + 'community:register', 'communityPackage:install', 'communityPackage:uninstall', 'communityPackage:update', diff --git a/packages/editor-ui/src/components/PersonalizationModal.vue b/packages/editor-ui/src/components/PersonalizationModal.vue index ee7e1ff4d3..53e40f1cfb 100644 --- a/packages/editor-ui/src/components/PersonalizationModal.vue +++ b/packages/editor-ui/src/components/PersonalizationModal.vue @@ -93,6 +93,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks'; import { useI18n } from '@/composables/useI18n'; import { useRoute, useRouter } from 'vue-router'; import { useUIStore } from '@/stores/ui.store'; +import { getResourcePermissions } from '@/permissions'; const SURVEY_VERSION = 'v4'; @@ -110,7 +111,9 @@ const uiStore = useUIStore(); const formValues = ref>({}); const isSaving = ref(false); - +const userPermissions = computed(() => + getResourcePermissions(usersStore.currentUser?.globalScopes), +); const survey = computed(() => [ { name: COMPANY_TYPE_KEY, @@ -548,23 +551,30 @@ const onSave = () => { formBus.emit('submit'); }; +const closeCallback = () => { + const isPartOfOnboardingExperiment = + posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) === + MORE_ONBOARDING_OPTIONS_EXPERIMENT.control; + // In case the redirect to homepage for new users didn't happen + // we try again after closing the modal + if (route.name !== VIEWS.HOMEPAGE && !isPartOfOnboardingExperiment) { + void router.replace({ name: VIEWS.HOMEPAGE }); + } +}; + const closeDialog = () => { modalBus.emit('close'); - uiStore.openModalWithData({ - name: COMMUNITY_PLUS_ENROLLMENT_MODAL, - data: { - closeCallback: () => { - const isPartOfOnboardingExperiment = - posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) === - MORE_ONBOARDING_OPTIONS_EXPERIMENT.control; - // In case the redirect to homepage for new users didn't happen - // we try again after closing the modal - if (route.name !== VIEWS.HOMEPAGE && !isPartOfOnboardingExperiment) { - void router.replace({ name: VIEWS.HOMEPAGE }); - } + + if (userPermissions.value.community.register) { + uiStore.openModalWithData({ + name: COMMUNITY_PLUS_ENROLLMENT_MODAL, + data: { + closeCallback, }, - }, - }); + }); + } else { + closeCallback(); + } }; const onSubmit = async (values: IPersonalizationLatestVersion) => { diff --git a/packages/editor-ui/src/permissions.spec.ts b/packages/editor-ui/src/permissions.spec.ts index e7946ce421..ab3952fbeb 100644 --- a/packages/editor-ui/src/permissions.spec.ts +++ b/packages/editor-ui/src/permissions.spec.ts @@ -8,6 +8,7 @@ describe('permissions', () => { annotationTag: {}, auditLogs: {}, banner: {}, + community: {}, communityPackage: {}, credential: {}, externalSecretsProvider: {}, @@ -62,6 +63,7 @@ describe('permissions', () => { annotationTag: {}, auditLogs: {}, banner: {}, + community: {}, communityPackage: {}, credential: { create: true, diff --git a/packages/editor-ui/src/stores/rbac.store.ts b/packages/editor-ui/src/stores/rbac.store.ts index d51bbc8538..a38b0f674b 100644 --- a/packages/editor-ui/src/stores/rbac.store.ts +++ b/packages/editor-ui/src/stores/rbac.store.ts @@ -27,6 +27,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => { eventBusDestination: {}, auditLogs: {}, banner: {}, + community: {}, communityPackage: {}, ldap: {}, license: {}, diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts b/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts index bd88c02228..e33d2a080e 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.test.ts @@ -6,6 +6,8 @@ import { useUsageStore } from '@/stores/usage.store'; import SettingsUsageAndPlan from '@/views/SettingsUsageAndPlan.vue'; import { useUIStore } from '@/stores/ui.store'; import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants'; +import { useUsersStore } from '@/stores/users.store'; +import type { IUser } from '@/Interface'; vi.mock('vue-router', () => { return { @@ -23,6 +25,7 @@ vi.mock('vue-router', () => { let usageStore: ReturnType>; let uiStore: ReturnType>; +let usersStore: ReturnType>; const renderComponent = createComponentRenderer(SettingsUsageAndPlan); @@ -31,6 +34,7 @@ describe('SettingsUsageAndPlan', () => { createTestingPinia(); usageStore = mockedStore(useUsageStore); uiStore = mockedStore(useUIStore); + usersStore = mockedStore(useUsersStore); usageStore.viewPlansUrl = 'https://subscription.n8n.io'; usageStore.managePlanUrl = 'https://subscription.n8n.io'; @@ -49,6 +53,9 @@ describe('SettingsUsageAndPlan', () => { it('should not show badge but unlock notice', async () => { usageStore.isLoading = false; usageStore.planName = 'Community'; + usersStore.currentUser = { + globalScopes: ['community:register'], + } as IUser; const { getByRole, container } = renderComponent(); expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community'); expect(container.querySelector('.n8n-badge')).toBeNull(); diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue index b1df9d2fa9..82a6da8b39 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue @@ -11,11 +11,14 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { hasPermission } from '@/utils/rbac/permissions'; import N8nInfoTip from 'n8n-design-system/components/N8nInfoTip'; import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants'; +import { useUsersStore } from '@/stores/users.store'; +import { getResourcePermissions } from '@/permissions'; const usageStore = useUsageStore(); const route = useRoute(); const router = useRouter(); const uiStore = useUIStore(); +const usersStore = useUsersStore(); const toast = useToast(); const documentTitle = useDocumentTitle(); @@ -48,6 +51,10 @@ const isCommunityEditionRegistered = computed( () => usageStore.planName.toLowerCase() === 'registered community', ); +const canUserRegisterCommunityPlus = computed( + () => getResourcePermissions(usersStore.currentUser?.globalScopes).community.register, +); + const showActivationSuccess = () => { toast.showMessage({ type: 'success', @@ -173,7 +180,7 @@ const openCommunityRegisterModal = () => { - +