fix(editor): Replace isInstanceOwner checks with scopes where applicable (#7858)
Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
@@ -184,6 +184,7 @@ import { getWorkflowPermissions } from '@/permissions';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { hasPermission } from '@/rbac/permissions';
|
||||
|
||||
const hasChanged = (prev: string[], curr: string[]) => {
|
||||
if (prev.length !== curr.length) {
|
||||
@@ -247,10 +248,7 @@ export default defineComponent({
|
||||
currentUser(): IUser | null {
|
||||
return this.usersStore.currentUser;
|
||||
},
|
||||
currentUserIsOwner(): boolean {
|
||||
return this.usersStore.currentUser?.isOwner ?? false;
|
||||
},
|
||||
contextBasedTranslationKeys(): NestedRecord<string> {
|
||||
contextBasedTranslationKeys() {
|
||||
return this.uiStore.contextBasedTranslationKeys;
|
||||
},
|
||||
isWorkflowActive(): boolean {
|
||||
@@ -298,7 +296,7 @@ export default defineComponent({
|
||||
].includes(this.$route.name || '');
|
||||
},
|
||||
workflowPermissions(): IPermissions {
|
||||
return getWorkflowPermissions(this.usersStore.currentUser, this.workflow);
|
||||
return getWorkflowPermissions(this.currentUser, this.workflow);
|
||||
},
|
||||
workflowMenuItems(): Array<{}> {
|
||||
const actions = [
|
||||
@@ -330,7 +328,7 @@ export default defineComponent({
|
||||
);
|
||||
}
|
||||
|
||||
if (this.currentUserIsOwner) {
|
||||
if (hasPermission(['rbac'], { rbac: { scope: 'sourceControl:push' } })) {
|
||||
actions.push({
|
||||
id: WORKFLOW_MENU_ACTIONS.PUSH,
|
||||
label: this.$locale.baseText('menuActions.push'),
|
||||
@@ -338,8 +336,7 @@ export default defineComponent({
|
||||
!this.sourceControlStore.isEnterpriseSourceControlEnabled ||
|
||||
!this.onWorkflowPage ||
|
||||
this.onExecutionsTab ||
|
||||
this.readOnlyEnv ||
|
||||
!this.currentUserIsOwner,
|
||||
this.readOnlyEnv,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ export default defineComponent({
|
||||
position: 'bottom',
|
||||
label: 'Admin Panel',
|
||||
icon: 'home',
|
||||
available: this.settingsStore.isCloudDeployment && this.usersStore.isInstanceOwner,
|
||||
available: this.settingsStore.isCloudDeployment && hasPermission(['instanceOwner']),
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
|
||||
@@ -3,12 +3,11 @@ import { computed, nextTick, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { hasPermission } from '@/rbac/permissions';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useLoadingService } from '@/composables/useLoadingService';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, VIEWS } from '@/constants';
|
||||
import type { SourceControlAggregatedFile } from '../Interface';
|
||||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||
@@ -24,9 +23,7 @@ const responseStatuses = {
|
||||
const router = useRouter();
|
||||
const loadingService = useLoadingService();
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const message = useMessage();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -36,8 +33,11 @@ const tooltipOpenDelay = ref(300);
|
||||
const currentBranch = computed(() => {
|
||||
return sourceControlStore.preferences.branchName;
|
||||
});
|
||||
const isInstanceOwner = computed(() => usersStore.isInstanceOwner);
|
||||
const setupButtonTooltipPlacement = computed(() => (props.isCollapsed ? 'right' : 'top'));
|
||||
const sourceControlAvailable = computed(
|
||||
() =>
|
||||
sourceControlStore.isEnterpriseSourceControlEnabled &&
|
||||
hasPermission(['rbac'], { rbac: { scope: 'sourceControl:manage' } }),
|
||||
);
|
||||
|
||||
async function pushWorkfolder() {
|
||||
loadingService.startLoading();
|
||||
@@ -125,7 +125,7 @@ const goToSourceControlSetup = async () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="sourceControlStore.isEnterpriseSourceControlEnabled && isInstanceOwner"
|
||||
v-if="sourceControlAvailable"
|
||||
:class="{
|
||||
[$style.sync]: true,
|
||||
[$style.collapsed]: isCollapsed,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<el-switch
|
||||
class="mr-s"
|
||||
:disabled="!isInstanceOwner"
|
||||
:disabled="readonly"
|
||||
:modelValue="nodeParameters.enabled"
|
||||
@update:modelValue="onEnabledSwitched($event, destination.id)"
|
||||
:title="
|
||||
@@ -84,7 +84,7 @@ export default defineComponent({
|
||||
required: true,
|
||||
default: deepCopy(defaultMessageEventBusDestinationOptions),
|
||||
},
|
||||
isInstanceOwner: Boolean,
|
||||
readonly: Boolean,
|
||||
},
|
||||
mounted() {
|
||||
this.nodeParameters = Object.assign(
|
||||
@@ -105,7 +105,7 @@ export default defineComponent({
|
||||
value: DESTINATION_LIST_ITEM_ACTIONS.OPEN,
|
||||
},
|
||||
];
|
||||
if (this.isInstanceOwner) {
|
||||
if (!this.readonly) {
|
||||
actions.push({
|
||||
label: this.$locale.baseText('workflows.item.delete'),
|
||||
value: DESTINATION_LIST_ITEM_ACTIONS.DELETE,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
@click="sendTestEvent"
|
||||
data-test-id="destination-test-button"
|
||||
/>
|
||||
<template v-if="isInstanceOwner">
|
||||
<template v-if="canManageLogStreaming">
|
||||
<n8n-icon-button
|
||||
v-if="nodeParameters && hasOnceBeenSaved"
|
||||
:title="$locale.baseText('settings.log-streaming.delete')"
|
||||
@@ -117,7 +117,7 @@
|
||||
:parameters="webhookDescription"
|
||||
:hideDelete="true"
|
||||
:nodeValues="nodeParameters"
|
||||
:isReadOnly="!isInstanceOwner"
|
||||
:isReadOnly="!canManageLogStreaming"
|
||||
path=""
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
@@ -127,7 +127,7 @@
|
||||
:parameters="syslogDescription"
|
||||
:hideDelete="true"
|
||||
:nodeValues="nodeParameters"
|
||||
:isReadOnly="!isInstanceOwner"
|
||||
:isReadOnly="!canManageLogStreaming"
|
||||
path=""
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
@@ -137,7 +137,7 @@
|
||||
:parameters="sentryDescription"
|
||||
:hideDelete="true"
|
||||
:nodeValues="nodeParameters"
|
||||
:isReadOnly="!isInstanceOwner"
|
||||
:isReadOnly="!canManageLogStreaming"
|
||||
path=""
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
@@ -156,7 +156,7 @@
|
||||
:destinationId="destination.id"
|
||||
@input="onInput"
|
||||
@change="valueChanged"
|
||||
:readonly="!isInstanceOwner"
|
||||
:readonly="!canManageLogStreaming"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,7 +194,7 @@ import { LOG_STREAM_MODAL_KEY, MODAL_CONFIRM } from '@/constants';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { hasPermission } from '@/rbac/permissions';
|
||||
import { destinationToFakeINodeUi } from '@/components/SettingsLogStreaming/Helpers.ee';
|
||||
import {
|
||||
webhookModalDescription,
|
||||
@@ -252,12 +252,11 @@ export default defineComponent({
|
||||
headerLabel: this.destination.label,
|
||||
testMessageSent: false,
|
||||
testMessageResult: false,
|
||||
isInstanceOwner: false,
|
||||
LOG_STREAM_MODAL_KEY,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUIStore, useUsersStore, useLogStreamingStore, useNDVStore, useWorkflowsStore),
|
||||
...mapStores(useUIStore, useLogStreamingStore, useNDVStore, useWorkflowsStore),
|
||||
typeSelectOptions(): Array<{ value: string; label: BaseTextKey }> {
|
||||
const options: Array<{ value: string; label: BaseTextKey }> = [];
|
||||
for (const t of Object.values(MessageEventBusDestinationTypeNames)) {
|
||||
@@ -306,9 +305,11 @@ export default defineComponent({
|
||||
}
|
||||
return items;
|
||||
},
|
||||
canManageLogStreaming(): boolean {
|
||||
return hasPermission(['rbac'], { rbac: { scope: 'logStreaming:manage' } });
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.isInstanceOwner = this.usersStore.currentUser?.globalRole?.name === 'owner';
|
||||
this.setupNode(
|
||||
Object.assign(deepCopy(defaultMessageEventBusDestinationOptions), this.destination),
|
||||
);
|
||||
|
||||
@@ -381,6 +381,8 @@ import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import type { IPermissions } from '@/permissions';
|
||||
import { getWorkflowPermissions } from '@/permissions';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WorkflowSettings',
|
||||
@@ -479,6 +481,9 @@ export default defineComponent({
|
||||
|
||||
return this.workflowsEEStore.getWorkflowOwnerName(`${this.workflowId}`, fallback);
|
||||
},
|
||||
workflowPermissions(): IPermissions {
|
||||
return getWorkflowPermissions(this.currentUser, this.workflow);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.executionTimeout = this.rootStore.executionTimeout;
|
||||
@@ -584,8 +589,6 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
async loadWorkflowCallerPolicyOptions() {
|
||||
const currentUserIsOwner = this.workflow.ownedBy?.id === this.currentUser?.id;
|
||||
|
||||
this.workflowCallerPolicyOptions = [
|
||||
{
|
||||
key: 'none',
|
||||
@@ -597,7 +600,7 @@ export default defineComponent({
|
||||
'workflowSettings.callerPolicy.options.workflowsFromSameOwner',
|
||||
{
|
||||
interpolate: {
|
||||
owner: currentUserIsOwner
|
||||
owner: this.workflowPermissions.isOwner
|
||||
? this.$locale.baseText(
|
||||
'workflowSettings.callerPolicy.options.workflowsFromSameOwner.owner',
|
||||
)
|
||||
|
||||
@@ -8,13 +8,13 @@ import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useRBACStore } from '@/stores/rbac.store';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let rbacStore: ReturnType<typeof useRBACStore>;
|
||||
|
||||
const renderComponent = createComponentRenderer(MainSidebarSourceControl);
|
||||
|
||||
@@ -28,8 +28,8 @@ describe('MainSidebarSourceControl', () => {
|
||||
},
|
||||
});
|
||||
|
||||
usersStore = useUsersStore(pinia);
|
||||
vi.spyOn(usersStore, 'isInstanceOwner', 'get').mockReturnValue(true);
|
||||
rbacStore = useRBACStore(pinia);
|
||||
vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true);
|
||||
|
||||
sourceControlStore = useSourceControlStore();
|
||||
vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true);
|
||||
@@ -38,7 +38,7 @@ describe('MainSidebarSourceControl', () => {
|
||||
});
|
||||
|
||||
it('should render nothing when not instance owner', async () => {
|
||||
vi.spyOn(usersStore, 'isInstanceOwner', 'get').mockReturnValue(false);
|
||||
vi.spyOn(rbacStore, 'hasScope').mockReturnValue(false);
|
||||
const { container } = renderComponent({ pinia, props: { isCollapsed: false } });
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||
import { i18n as locale } from '@/plugins/i18n';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { hasPermission } from '@/rbac/permissions';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
|
||||
async function dismissPermanently() {
|
||||
await uiStore.dismissBanner('V1', 'permanent');
|
||||
}
|
||||
|
||||
const hasOwnerPermission = computed(() => hasPermission(['instanceOwner']));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,7 +19,7 @@ async function dismissPermanently() {
|
||||
<template #mainContent>
|
||||
<span v-html="locale.baseText('banners.v1.message')"></span>
|
||||
<a
|
||||
v-if="usersStore.isInstanceOwner"
|
||||
v-if="hasOwnerPermission"
|
||||
:class="$style.link"
|
||||
@click="dismissPermanently"
|
||||
data-test-id="banner-confirm-v1"
|
||||
|
||||
Reference in New Issue
Block a user