mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
fix(editor): Replace isInstanceOwner checks with scopes where applicable (#7858)
Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
parent
39fa8d21bb
commit
132d691cbf
|
@ -7,25 +7,25 @@ const CONTACT_EMAIL_SUBMISSION_ENDPOINT = '/accounts/onboarding';
|
||||||
|
|
||||||
export async function fetchNextOnboardingPrompt(
|
export async function fetchNextOnboardingPrompt(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
currentUer: IUser,
|
currentUser: IUser,
|
||||||
): Promise<IOnboardingCallPrompt> {
|
): Promise<IOnboardingCallPrompt> {
|
||||||
return get(N8N_API_BASE_URL, ONBOARDING_PROMPTS_ENDPOINT, {
|
return get(N8N_API_BASE_URL, ONBOARDING_PROMPTS_ENDPOINT, {
|
||||||
instance_id: instanceId,
|
instance_id: instanceId,
|
||||||
user_id: `${instanceId}#${currentUer.id}`,
|
user_id: `${instanceId}#${currentUser.id}`,
|
||||||
is_owner: currentUer.isOwner,
|
is_owner: currentUser.isOwner,
|
||||||
survey_results: currentUer.personalizationAnswers,
|
survey_results: currentUser.personalizationAnswers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyForOnboardingCall(
|
export async function applyForOnboardingCall(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
currentUer: IUser,
|
currentUser: IUser,
|
||||||
email: string,
|
email: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const response = await post(N8N_API_BASE_URL, ONBOARDING_PROMPTS_ENDPOINT, {
|
const response = await post(N8N_API_BASE_URL, ONBOARDING_PROMPTS_ENDPOINT, {
|
||||||
instance_id: instanceId,
|
instance_id: instanceId,
|
||||||
user_id: `${instanceId}#${currentUer.id}`,
|
user_id: `${instanceId}#${currentUser.id}`,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
|
@ -36,13 +36,13 @@ export async function applyForOnboardingCall(
|
||||||
|
|
||||||
export async function submitEmailOnSignup(
|
export async function submitEmailOnSignup(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
currentUer: IUser,
|
currentUser: IUser,
|
||||||
email: string | undefined,
|
email: string | undefined,
|
||||||
agree: boolean,
|
agree: boolean,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return post(N8N_API_BASE_URL, CONTACT_EMAIL_SUBMISSION_ENDPOINT, {
|
return post(N8N_API_BASE_URL, CONTACT_EMAIL_SUBMISSION_ENDPOINT, {
|
||||||
instance_id: instanceId,
|
instance_id: instanceId,
|
||||||
user_id: `${instanceId}#${currentUer.id}`,
|
user_id: `${instanceId}#${currentUser.id}`,
|
||||||
email,
|
email,
|
||||||
agree,
|
agree,
|
||||||
});
|
});
|
||||||
|
|
|
@ -184,6 +184,7 @@ import { getWorkflowPermissions } from '@/permissions';
|
||||||
import { createEventBus } from 'n8n-design-system/utils';
|
import { createEventBus } from 'n8n-design-system/utils';
|
||||||
import { nodeViewEventBus } from '@/event-bus';
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||||
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
|
|
||||||
const hasChanged = (prev: string[], curr: string[]) => {
|
const hasChanged = (prev: string[], curr: string[]) => {
|
||||||
if (prev.length !== curr.length) {
|
if (prev.length !== curr.length) {
|
||||||
|
@ -247,10 +248,7 @@ export default defineComponent({
|
||||||
currentUser(): IUser | null {
|
currentUser(): IUser | null {
|
||||||
return this.usersStore.currentUser;
|
return this.usersStore.currentUser;
|
||||||
},
|
},
|
||||||
currentUserIsOwner(): boolean {
|
contextBasedTranslationKeys() {
|
||||||
return this.usersStore.currentUser?.isOwner ?? false;
|
|
||||||
},
|
|
||||||
contextBasedTranslationKeys(): NestedRecord<string> {
|
|
||||||
return this.uiStore.contextBasedTranslationKeys;
|
return this.uiStore.contextBasedTranslationKeys;
|
||||||
},
|
},
|
||||||
isWorkflowActive(): boolean {
|
isWorkflowActive(): boolean {
|
||||||
|
@ -298,7 +296,7 @@ export default defineComponent({
|
||||||
].includes(this.$route.name || '');
|
].includes(this.$route.name || '');
|
||||||
},
|
},
|
||||||
workflowPermissions(): IPermissions {
|
workflowPermissions(): IPermissions {
|
||||||
return getWorkflowPermissions(this.usersStore.currentUser, this.workflow);
|
return getWorkflowPermissions(this.currentUser, this.workflow);
|
||||||
},
|
},
|
||||||
workflowMenuItems(): Array<{}> {
|
workflowMenuItems(): Array<{}> {
|
||||||
const actions = [
|
const actions = [
|
||||||
|
@ -330,7 +328,7 @@ export default defineComponent({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentUserIsOwner) {
|
if (hasPermission(['rbac'], { rbac: { scope: 'sourceControl:push' } })) {
|
||||||
actions.push({
|
actions.push({
|
||||||
id: WORKFLOW_MENU_ACTIONS.PUSH,
|
id: WORKFLOW_MENU_ACTIONS.PUSH,
|
||||||
label: this.$locale.baseText('menuActions.push'),
|
label: this.$locale.baseText('menuActions.push'),
|
||||||
|
@ -338,8 +336,7 @@ export default defineComponent({
|
||||||
!this.sourceControlStore.isEnterpriseSourceControlEnabled ||
|
!this.sourceControlStore.isEnterpriseSourceControlEnabled ||
|
||||||
!this.onWorkflowPage ||
|
!this.onWorkflowPage ||
|
||||||
this.onExecutionsTab ||
|
this.onExecutionsTab ||
|
||||||
this.readOnlyEnv ||
|
this.readOnlyEnv,
|
||||||
!this.currentUserIsOwner,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -266,7 +266,7 @@ export default defineComponent({
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
label: 'Admin Panel',
|
label: 'Admin Panel',
|
||||||
icon: 'home',
|
icon: 'home',
|
||||||
available: this.settingsStore.isCloudDeployment && this.usersStore.isInstanceOwner,
|
available: this.settingsStore.isCloudDeployment && hasPermission(['instanceOwner']),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'settings',
|
id: 'settings',
|
||||||
|
|
|
@ -3,12 +3,11 @@ import { computed, nextTick, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { createEventBus } from 'n8n-design-system/utils';
|
import { createEventBus } from 'n8n-design-system/utils';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useLoadingService } from '@/composables/useLoadingService';
|
import { useLoadingService } from '@/composables/useLoadingService';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.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 { SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, VIEWS } from '@/constants';
|
||||||
import type { SourceControlAggregatedFile } from '../Interface';
|
import type { SourceControlAggregatedFile } from '../Interface';
|
||||||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||||
|
@ -24,9 +23,7 @@ const responseStatuses = {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loadingService = useLoadingService();
|
const loadingService = useLoadingService();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const usersStore = useUsersStore();
|
|
||||||
const sourceControlStore = useSourceControlStore();
|
const sourceControlStore = useSourceControlStore();
|
||||||
const message = useMessage();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
@ -36,8 +33,11 @@ const tooltipOpenDelay = ref(300);
|
||||||
const currentBranch = computed(() => {
|
const currentBranch = computed(() => {
|
||||||
return sourceControlStore.preferences.branchName;
|
return sourceControlStore.preferences.branchName;
|
||||||
});
|
});
|
||||||
const isInstanceOwner = computed(() => usersStore.isInstanceOwner);
|
const sourceControlAvailable = computed(
|
||||||
const setupButtonTooltipPlacement = computed(() => (props.isCollapsed ? 'right' : 'top'));
|
() =>
|
||||||
|
sourceControlStore.isEnterpriseSourceControlEnabled &&
|
||||||
|
hasPermission(['rbac'], { rbac: { scope: 'sourceControl:manage' } }),
|
||||||
|
);
|
||||||
|
|
||||||
async function pushWorkfolder() {
|
async function pushWorkfolder() {
|
||||||
loadingService.startLoading();
|
loadingService.startLoading();
|
||||||
|
@ -125,7 +125,7 @@ const goToSourceControlSetup = async () => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="sourceControlStore.isEnterpriseSourceControlEnabled && isInstanceOwner"
|
v-if="sourceControlAvailable"
|
||||||
:class="{
|
:class="{
|
||||||
[$style.sync]: true,
|
[$style.sync]: true,
|
||||||
[$style.collapsed]: isCollapsed,
|
[$style.collapsed]: isCollapsed,
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
<el-switch
|
<el-switch
|
||||||
class="mr-s"
|
class="mr-s"
|
||||||
:disabled="!isInstanceOwner"
|
:disabled="readonly"
|
||||||
:modelValue="nodeParameters.enabled"
|
:modelValue="nodeParameters.enabled"
|
||||||
@update:modelValue="onEnabledSwitched($event, destination.id)"
|
@update:modelValue="onEnabledSwitched($event, destination.id)"
|
||||||
:title="
|
:title="
|
||||||
|
@ -84,7 +84,7 @@ export default defineComponent({
|
||||||
required: true,
|
required: true,
|
||||||
default: deepCopy(defaultMessageEventBusDestinationOptions),
|
default: deepCopy(defaultMessageEventBusDestinationOptions),
|
||||||
},
|
},
|
||||||
isInstanceOwner: Boolean,
|
readonly: Boolean,
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.nodeParameters = Object.assign(
|
this.nodeParameters = Object.assign(
|
||||||
|
@ -105,7 +105,7 @@ export default defineComponent({
|
||||||
value: DESTINATION_LIST_ITEM_ACTIONS.OPEN,
|
value: DESTINATION_LIST_ITEM_ACTIONS.OPEN,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (this.isInstanceOwner) {
|
if (!this.readonly) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: this.$locale.baseText('workflows.item.delete'),
|
label: this.$locale.baseText('workflows.item.delete'),
|
||||||
value: DESTINATION_LIST_ITEM_ACTIONS.DELETE,
|
value: DESTINATION_LIST_ITEM_ACTIONS.DELETE,
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
@click="sendTestEvent"
|
@click="sendTestEvent"
|
||||||
data-test-id="destination-test-button"
|
data-test-id="destination-test-button"
|
||||||
/>
|
/>
|
||||||
<template v-if="isInstanceOwner">
|
<template v-if="canManageLogStreaming">
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
v-if="nodeParameters && hasOnceBeenSaved"
|
v-if="nodeParameters && hasOnceBeenSaved"
|
||||||
:title="$locale.baseText('settings.log-streaming.delete')"
|
:title="$locale.baseText('settings.log-streaming.delete')"
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
:parameters="webhookDescription"
|
:parameters="webhookDescription"
|
||||||
:hideDelete="true"
|
:hideDelete="true"
|
||||||
:nodeValues="nodeParameters"
|
:nodeValues="nodeParameters"
|
||||||
:isReadOnly="!isInstanceOwner"
|
:isReadOnly="!canManageLogStreaming"
|
||||||
path=""
|
path=""
|
||||||
@valueChanged="valueChanged"
|
@valueChanged="valueChanged"
|
||||||
/>
|
/>
|
||||||
|
@ -127,7 +127,7 @@
|
||||||
:parameters="syslogDescription"
|
:parameters="syslogDescription"
|
||||||
:hideDelete="true"
|
:hideDelete="true"
|
||||||
:nodeValues="nodeParameters"
|
:nodeValues="nodeParameters"
|
||||||
:isReadOnly="!isInstanceOwner"
|
:isReadOnly="!canManageLogStreaming"
|
||||||
path=""
|
path=""
|
||||||
@valueChanged="valueChanged"
|
@valueChanged="valueChanged"
|
||||||
/>
|
/>
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
:parameters="sentryDescription"
|
:parameters="sentryDescription"
|
||||||
:hideDelete="true"
|
:hideDelete="true"
|
||||||
:nodeValues="nodeParameters"
|
:nodeValues="nodeParameters"
|
||||||
:isReadOnly="!isInstanceOwner"
|
:isReadOnly="!canManageLogStreaming"
|
||||||
path=""
|
path=""
|
||||||
@valueChanged="valueChanged"
|
@valueChanged="valueChanged"
|
||||||
/>
|
/>
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
:destinationId="destination.id"
|
:destinationId="destination.id"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@change="valueChanged"
|
@change="valueChanged"
|
||||||
:readonly="!isInstanceOwner"
|
:readonly="!canManageLogStreaming"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -194,7 +194,7 @@ import { LOG_STREAM_MODAL_KEY, MODAL_CONFIRM } from '@/constants';
|
||||||
import Modal from '@/components/Modal.vue';
|
import Modal from '@/components/Modal.vue';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
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 { destinationToFakeINodeUi } from '@/components/SettingsLogStreaming/Helpers.ee';
|
||||||
import {
|
import {
|
||||||
webhookModalDescription,
|
webhookModalDescription,
|
||||||
|
@ -252,12 +252,11 @@ export default defineComponent({
|
||||||
headerLabel: this.destination.label,
|
headerLabel: this.destination.label,
|
||||||
testMessageSent: false,
|
testMessageSent: false,
|
||||||
testMessageResult: false,
|
testMessageResult: false,
|
||||||
isInstanceOwner: false,
|
|
||||||
LOG_STREAM_MODAL_KEY,
|
LOG_STREAM_MODAL_KEY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useUIStore, useUsersStore, useLogStreamingStore, useNDVStore, useWorkflowsStore),
|
...mapStores(useUIStore, useLogStreamingStore, useNDVStore, useWorkflowsStore),
|
||||||
typeSelectOptions(): Array<{ value: string; label: BaseTextKey }> {
|
typeSelectOptions(): Array<{ value: string; label: BaseTextKey }> {
|
||||||
const options: Array<{ value: string; label: BaseTextKey }> = [];
|
const options: Array<{ value: string; label: BaseTextKey }> = [];
|
||||||
for (const t of Object.values(MessageEventBusDestinationTypeNames)) {
|
for (const t of Object.values(MessageEventBusDestinationTypeNames)) {
|
||||||
|
@ -306,9 +305,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
|
canManageLogStreaming(): boolean {
|
||||||
|
return hasPermission(['rbac'], { rbac: { scope: 'logStreaming:manage' } });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.isInstanceOwner = this.usersStore.currentUser?.globalRole?.name === 'owner';
|
|
||||||
this.setupNode(
|
this.setupNode(
|
||||||
Object.assign(deepCopy(defaultMessageEventBusDestinationOptions), this.destination),
|
Object.assign(deepCopy(defaultMessageEventBusDestinationOptions), this.destination),
|
||||||
);
|
);
|
||||||
|
|
|
@ -381,6 +381,8 @@ import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { createEventBus } from 'n8n-design-system/utils';
|
import { createEventBus } from 'n8n-design-system/utils';
|
||||||
|
import type { IPermissions } from '@/permissions';
|
||||||
|
import { getWorkflowPermissions } from '@/permissions';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'WorkflowSettings',
|
name: 'WorkflowSettings',
|
||||||
|
@ -479,6 +481,9 @@ export default defineComponent({
|
||||||
|
|
||||||
return this.workflowsEEStore.getWorkflowOwnerName(`${this.workflowId}`, fallback);
|
return this.workflowsEEStore.getWorkflowOwnerName(`${this.workflowId}`, fallback);
|
||||||
},
|
},
|
||||||
|
workflowPermissions(): IPermissions {
|
||||||
|
return getWorkflowPermissions(this.currentUser, this.workflow);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.executionTimeout = this.rootStore.executionTimeout;
|
this.executionTimeout = this.rootStore.executionTimeout;
|
||||||
|
@ -584,8 +589,6 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async loadWorkflowCallerPolicyOptions() {
|
async loadWorkflowCallerPolicyOptions() {
|
||||||
const currentUserIsOwner = this.workflow.ownedBy?.id === this.currentUser?.id;
|
|
||||||
|
|
||||||
this.workflowCallerPolicyOptions = [
|
this.workflowCallerPolicyOptions = [
|
||||||
{
|
{
|
||||||
key: 'none',
|
key: 'none',
|
||||||
|
@ -597,7 +600,7 @@ export default defineComponent({
|
||||||
'workflowSettings.callerPolicy.options.workflowsFromSameOwner',
|
'workflowSettings.callerPolicy.options.workflowsFromSameOwner',
|
||||||
{
|
{
|
||||||
interpolate: {
|
interpolate: {
|
||||||
owner: currentUserIsOwner
|
owner: this.workflowPermissions.isOwner
|
||||||
? this.$locale.baseText(
|
? this.$locale.baseText(
|
||||||
'workflowSettings.callerPolicy.options.workflowsFromSameOwner.owner',
|
'workflowSettings.callerPolicy.options.workflowsFromSameOwner.owner',
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,13 +8,13 @@ import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
|
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useRBACStore } from '@/stores/rbac.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
||||||
let pinia: ReturnType<typeof createTestingPinia>;
|
let pinia: ReturnType<typeof createTestingPinia>;
|
||||||
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
|
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
|
||||||
let uiStore: ReturnType<typeof useUIStore>;
|
let uiStore: ReturnType<typeof useUIStore>;
|
||||||
let usersStore: ReturnType<typeof useUsersStore>;
|
let rbacStore: ReturnType<typeof useRBACStore>;
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(MainSidebarSourceControl);
|
const renderComponent = createComponentRenderer(MainSidebarSourceControl);
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ describe('MainSidebarSourceControl', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
usersStore = useUsersStore(pinia);
|
rbacStore = useRBACStore(pinia);
|
||||||
vi.spyOn(usersStore, 'isInstanceOwner', 'get').mockReturnValue(true);
|
vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true);
|
||||||
|
|
||||||
sourceControlStore = useSourceControlStore();
|
sourceControlStore = useSourceControlStore();
|
||||||
vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true);
|
vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true);
|
||||||
|
@ -38,7 +38,7 @@ describe('MainSidebarSourceControl', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render nothing when not instance owner', async () => {
|
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 } });
|
const { container } = renderComponent({ pinia, props: { isCollapsed: false } });
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const usersStore = useUsersStore();
|
|
||||||
|
|
||||||
async function dismissPermanently() {
|
async function dismissPermanently() {
|
||||||
await uiStore.dismissBanner('V1', 'permanent');
|
await uiStore.dismissBanner('V1', 'permanent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasOwnerPermission = computed(() => hasPermission(['instanceOwner']));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -17,7 +19,7 @@ async function dismissPermanently() {
|
||||||
<template #mainContent>
|
<template #mainContent>
|
||||||
<span v-html="locale.baseText('banners.v1.message')"></span>
|
<span v-html="locale.baseText('banners.v1.message')"></span>
|
||||||
<a
|
<a
|
||||||
v-if="usersStore.isInstanceOwner"
|
v-if="hasOwnerPermission"
|
||||||
:class="$style.link"
|
:class="$style.link"
|
||||||
@click="dismissPermanently"
|
@click="dismissPermanently"
|
||||||
data-test-id="banner-confirm-v1"
|
data-test-id="banner-confirm-v1"
|
||||||
|
|
|
@ -38,12 +38,13 @@ import { isObject } from '@/utils/objectUtils';
|
||||||
import { getCredentialPermissions } from '@/permissions';
|
import { getCredentialPermissions } from '@/permissions';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
|
||||||
export const nodeHelpers = defineComponent({
|
export const nodeHelpers = defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -53,7 +54,6 @@ export const nodeHelpers = defineComponent({
|
||||||
useNodeTypesStore,
|
useNodeTypesStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useWorkflowsStore,
|
useWorkflowsStore,
|
||||||
useUsersStore,
|
|
||||||
useRootStore,
|
useRootStore,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -475,11 +475,13 @@ export const nodeHelpers = defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameMatches.length === 0) {
|
if (nameMatches.length === 0) {
|
||||||
const isInstanceOwner = this.usersStore.isInstanceOwner;
|
|
||||||
const isCredentialUsedInWorkflow =
|
const isCredentialUsedInWorkflow =
|
||||||
this.workflowsStore.usedCredentials?.[selectedCredentials.id as string];
|
this.workflowsStore.usedCredentials?.[selectedCredentials.id as string];
|
||||||
|
|
||||||
if (!isCredentialUsedInWorkflow && !isInstanceOwner) {
|
if (
|
||||||
|
!isCredentialUsedInWorkflow &&
|
||||||
|
!hasPermission(['rbac'], { rbac: { scope: 'credential:read' } })
|
||||||
|
) {
|
||||||
foundIssues[credentialTypeDescription.name] = [
|
foundIssues[credentialTypeDescription.name] = [
|
||||||
this.$locale.baseText('nodeIssues.credentials.doNotExist', {
|
this.$locale.baseText('nodeIssues.credentials.doNotExist', {
|
||||||
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
|
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
|
||||||
|
|
|
@ -6,6 +6,7 @@ vi.mock('@/rbac/checks', () => ({
|
||||||
hasScope: vi.fn(),
|
hasScope: vi.fn(),
|
||||||
isGuest: vi.fn(),
|
isGuest: vi.fn(),
|
||||||
isDefaultUser: vi.fn(),
|
isDefaultUser: vi.fn(),
|
||||||
|
isInstanceOwner: vi.fn(),
|
||||||
isAuthenticated: vi.fn(),
|
isAuthenticated: vi.fn(),
|
||||||
isEnterpriseFeatureEnabled: vi.fn(),
|
isEnterpriseFeatureEnabled: vi.fn(),
|
||||||
isValid: vi.fn(),
|
isValid: vi.fn(),
|
||||||
|
@ -17,6 +18,7 @@ describe('hasPermission()', () => {
|
||||||
vi.mocked(checks.hasScope).mockReturnValue(true);
|
vi.mocked(checks.hasScope).mockReturnValue(true);
|
||||||
vi.mocked(checks.isGuest).mockReturnValue(true);
|
vi.mocked(checks.isGuest).mockReturnValue(true);
|
||||||
vi.mocked(checks.isDefaultUser).mockReturnValue(true);
|
vi.mocked(checks.isDefaultUser).mockReturnValue(true);
|
||||||
|
vi.mocked(checks.isInstanceOwner).mockReturnValue(true);
|
||||||
vi.mocked(checks.isAuthenticated).mockReturnValue(true);
|
vi.mocked(checks.isAuthenticated).mockReturnValue(true);
|
||||||
vi.mocked(checks.isEnterpriseFeatureEnabled).mockReturnValue(true);
|
vi.mocked(checks.isEnterpriseFeatureEnabled).mockReturnValue(true);
|
||||||
vi.mocked(checks.isValid).mockReturnValue(true);
|
vi.mocked(checks.isValid).mockReturnValue(true);
|
||||||
|
@ -30,6 +32,7 @@ describe('hasPermission()', () => {
|
||||||
'rbac',
|
'rbac',
|
||||||
'role',
|
'role',
|
||||||
'defaultUser',
|
'defaultUser',
|
||||||
|
'instanceOwner',
|
||||||
]),
|
]),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { isInstanceOwner } from '@/rbac/checks/isInstanceOwner';
|
||||||
|
|
||||||
|
vi.mock('@/stores/users.store', () => ({
|
||||||
|
useUsersStore: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Checks', () => {
|
||||||
|
describe('isInstanceOwner()', () => {
|
||||||
|
it('should return false if user not logged in', () => {
|
||||||
|
vi.mocked(useUsersStore).mockReturnValue({ isInstanceOwner: false } as ReturnType<
|
||||||
|
typeof useUsersStore
|
||||||
|
>);
|
||||||
|
|
||||||
|
expect(isInstanceOwner()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if user is default user', () => {
|
||||||
|
vi.mocked(useUsersStore).mockReturnValue({ isInstanceOwner: true } as unknown as ReturnType<
|
||||||
|
typeof useUsersStore
|
||||||
|
>);
|
||||||
|
|
||||||
|
expect(isInstanceOwner()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,6 +2,7 @@ export * from './hasRole';
|
||||||
export * from './hasScope';
|
export * from './hasScope';
|
||||||
export * from './isAuthenticated';
|
export * from './isAuthenticated';
|
||||||
export * from './isDefaultUser';
|
export * from './isDefaultUser';
|
||||||
|
export * from './isInstanceOwner';
|
||||||
export * from './isEnterpriseFeatureEnabled';
|
export * from './isEnterpriseFeatureEnabled';
|
||||||
export * from './isGuest';
|
export * from './isGuest';
|
||||||
export * from './isValid';
|
export * from './isValid';
|
||||||
|
|
5
packages/editor-ui/src/rbac/checks/isInstanceOwner.ts
Normal file
5
packages/editor-ui/src/rbac/checks/isInstanceOwner.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import type { DefaultUserMiddlewareOptions, RBACPermissionCheck } from '@/types/rbac';
|
||||||
|
|
||||||
|
export const isInstanceOwner: RBACPermissionCheck<DefaultUserMiddlewareOptions> = () =>
|
||||||
|
useUsersStore().isInstanceOwner;
|
|
@ -3,6 +3,7 @@ import {
|
||||||
hasScope,
|
hasScope,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isDefaultUser,
|
isDefaultUser,
|
||||||
|
isInstanceOwner,
|
||||||
isEnterpriseFeatureEnabled,
|
isEnterpriseFeatureEnabled,
|
||||||
isGuest,
|
isGuest,
|
||||||
isValid,
|
isValid,
|
||||||
|
@ -17,6 +18,7 @@ export const permissions: Permissions = {
|
||||||
authenticated: isAuthenticated,
|
authenticated: isAuthenticated,
|
||||||
custom: isValid,
|
custom: isValid,
|
||||||
defaultUser: isDefaultUser,
|
defaultUser: isDefaultUser,
|
||||||
|
instanceOwner: isInstanceOwner,
|
||||||
enterprise: isEnterpriseFeatureEnabled,
|
enterprise: isEnterpriseFeatureEnabled,
|
||||||
guest: isGuest,
|
guest: isGuest,
|
||||||
rbac: hasScope,
|
rbac: hasScope,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { useUsersStore } from '@/stores/users.store';
|
||||||
import { getAdminPanelLoginCode, getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
|
import { getAdminPanelLoginCode, getCurrentPlan, getCurrentUsage } from '@/api/cloudPlans';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { CLOUD_TRIAL_CHECK_INTERVAL, STORES } from '@/constants';
|
import { CLOUD_TRIAL_CHECK_INTERVAL, STORES } from '@/constants';
|
||||||
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
|
|
||||||
const DEFAULT_STATE: CloudPlanState = {
|
const DEFAULT_STATE: CloudPlanState = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
|
@ -55,13 +56,13 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
|
||||||
|
|
||||||
const hasCloudPlan = computed(() => {
|
const hasCloudPlan = computed(() => {
|
||||||
const cloudUserId = settingsStore.settings.n8nMetadata?.userId;
|
const cloudUserId = settingsStore.settings.n8nMetadata?.userId;
|
||||||
return usersStore.isInstanceOwner && settingsStore.isCloudDeployment && cloudUserId;
|
return hasPermission(['instanceOwner']) && settingsStore.isCloudDeployment && cloudUserId;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUserCloudAccount = async () => {
|
const getUserCloudAccount = async () => {
|
||||||
if (!hasCloudPlan.value) throw new Error('User does not have a cloud plan');
|
if (!hasCloudPlan.value) throw new Error('User does not have a cloud plan');
|
||||||
try {
|
try {
|
||||||
if (useUsersStore().isInstanceOwner) {
|
if (hasPermission(['instanceOwner'])) {
|
||||||
await usersStore.fetchUserCloudAccount();
|
await usersStore.fetchUserCloudAccount();
|
||||||
if (!usersStore.currentUserCloudInfo?.confirmed && !userIsTrialing.value) {
|
if (!usersStore.currentUserCloudInfo?.confirmed && !userIsTrialing.value) {
|
||||||
useUIStore().pushBannerToStack('EMAIL_CONFIRMATION');
|
useUIStore().pushBannerToStack('EMAIL_CONFIRMATION');
|
||||||
|
|
|
@ -5,13 +5,13 @@ import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import * as externalSecretsApi from '@/api/externalSecrets.ee';
|
import * as externalSecretsApi from '@/api/externalSecrets.ee';
|
||||||
import { connectProvider } from '@/api/externalSecrets.ee';
|
import { connectProvider } from '@/api/externalSecrets.ee';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useRBACStore } from '@/stores/rbac.store';
|
||||||
import type { ExternalSecretsProvider } from '@/Interface';
|
import type { ExternalSecretsProvider } from '@/Interface';
|
||||||
|
|
||||||
export const useExternalSecretsStore = defineStore('externalSecrets', () => {
|
export const useExternalSecretsStore = defineStore('externalSecrets', () => {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const usersStore = useUsersStore();
|
const rbacStore = useRBACStore();
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
providers: [] as ExternalSecretsProvider[],
|
providers: [] as ExternalSecretsProvider[],
|
||||||
|
@ -65,7 +65,7 @@ export const useExternalSecretsStore = defineStore('externalSecrets', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchAllSecrets() {
|
async function fetchAllSecrets() {
|
||||||
if (usersStore.isInstanceOwner) {
|
if (rbacStore.hasScope('externalSecret:list')) {
|
||||||
try {
|
try {
|
||||||
state.secrets = await externalSecretsApi.getExternalSecrets(rootStore.getRestApiContext);
|
state.secrets = await externalSecretsApi.getExternalSecrets(rootStore.getRestApiContext);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -60,7 +60,7 @@ import { getCurlToJson } from '@/api/curlHelper';
|
||||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
import { useTelemetryStore } from '@/stores/telemetry.store';
|
import { useTelemetryStore } from '@/stores/telemetry.store';
|
||||||
import { dismissBannerPermanently } from '@/api/ui';
|
import { dismissBannerPermanently } from '@/api/ui';
|
||||||
import type { BannerName } from 'n8n-workflow';
|
import type { BannerName } from 'n8n-workflow';
|
||||||
|
@ -374,9 +374,7 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
const isOwner = useUsersStore().isInstanceOwner;
|
if (deploymentType === 'cloud' && hasPermission(['instanceOwner'])) {
|
||||||
|
|
||||||
if (deploymentType === 'cloud' && isOwner) {
|
|
||||||
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
|
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
|
||||||
const { code } = await useCloudPlanStore().getAutoLoginCode();
|
const { code } = await useCloudPlanStore().getAutoLoginCode();
|
||||||
linkUrl = `https://${adminPanelHost}/login`;
|
linkUrl = `https://${adminPanelHost}/login`;
|
||||||
|
|
|
@ -137,7 +137,7 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
: undefined,
|
: undefined,
|
||||||
isDefaultUser: isDefaultUser(updatedUser),
|
isDefaultUser: isDefaultUser(updatedUser),
|
||||||
isPendingUser: isPendingUser(updatedUser),
|
isPendingUser: isPendingUser(updatedUser),
|
||||||
isOwner: updatedUser.globalRole?.name === ROLE.Owner,
|
isOwner: isInstanceOwner(updatedUser),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.users = {
|
this.users = {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { IRole } from '@/Interface';
|
||||||
export type AuthenticatedPermissionOptions = {};
|
export type AuthenticatedPermissionOptions = {};
|
||||||
export type CustomPermissionOptions<C = {}> = RBACPermissionCheck<C>;
|
export type CustomPermissionOptions<C = {}> = RBACPermissionCheck<C>;
|
||||||
export type DefaultUserMiddlewareOptions = {};
|
export type DefaultUserMiddlewareOptions = {};
|
||||||
|
export type InstanceOwnerMiddlewareOptions = {};
|
||||||
export type EnterprisePermissionOptions = {
|
export type EnterprisePermissionOptions = {
|
||||||
feature?: EnterpriseEditionFeature | EnterpriseEditionFeature[];
|
feature?: EnterpriseEditionFeature | EnterpriseEditionFeature[];
|
||||||
mode?: 'oneOf' | 'allOf';
|
mode?: 'oneOf' | 'allOf';
|
||||||
|
@ -23,6 +24,7 @@ export type PermissionType =
|
||||||
| 'authenticated'
|
| 'authenticated'
|
||||||
| 'custom'
|
| 'custom'
|
||||||
| 'defaultUser'
|
| 'defaultUser'
|
||||||
|
| 'instanceOwner'
|
||||||
| 'enterprise'
|
| 'enterprise'
|
||||||
| 'guest'
|
| 'guest'
|
||||||
| 'rbac'
|
| 'rbac'
|
||||||
|
@ -31,6 +33,7 @@ export type PermissionTypeOptions = {
|
||||||
authenticated: AuthenticatedPermissionOptions;
|
authenticated: AuthenticatedPermissionOptions;
|
||||||
custom: CustomPermissionOptions;
|
custom: CustomPermissionOptions;
|
||||||
defaultUser: DefaultUserMiddlewareOptions;
|
defaultUser: DefaultUserMiddlewareOptions;
|
||||||
|
instanceOwner: InstanceOwnerMiddlewareOptions;
|
||||||
enterprise: EnterprisePermissionOptions;
|
enterprise: EnterprisePermissionOptions;
|
||||||
guest: GuestPermissionOptions;
|
guest: GuestPermissionOptions;
|
||||||
rbac: RBACPermissionOptions;
|
rbac: RBACPermissionOptions;
|
||||||
|
|
|
@ -28,14 +28,14 @@
|
||||||
<event-destination-card
|
<event-destination-card
|
||||||
:destination="logStreamingStore.items[item.key]?.destination"
|
:destination="logStreamingStore.items[item.key]?.destination"
|
||||||
:eventBus="eventBus"
|
:eventBus="eventBus"
|
||||||
:isInstanceOwner="isInstanceOwner"
|
:readonly="!canManageLogStreaming"
|
||||||
@remove="onRemove(logStreamingStore.items[item.key]?.destination?.id)"
|
@remove="onRemove(logStreamingStore.items[item.key]?.destination?.id)"
|
||||||
@edit="onEdit(logStreamingStore.items[item.key]?.destination?.id)"
|
@edit="onEdit(logStreamingStore.items[item.key]?.destination?.id)"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<div class="mt-m text-right">
|
<div class="mt-m text-right">
|
||||||
<n8n-button v-if="isInstanceOwner" size="large" @click="addDestination">
|
<n8n-button v-if="canManageLogStreaming" size="large" @click="addDestination">
|
||||||
{{ $locale.baseText(`settings.log-streaming.add`) }}
|
{{ $locale.baseText(`settings.log-streaming.add`) }}
|
||||||
</n8n-button>
|
</n8n-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +77,7 @@ import { defineComponent, nextTick } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { hasPermission } from '@/rbac/permissions';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { useLogStreamingStore } from '@/stores/logStreaming.store';
|
import { useLogStreamingStore } from '@/stores/logStreaming.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
@ -100,13 +100,11 @@ export default defineComponent({
|
||||||
destinations: Array<MessageEventBusDestinationOptions>,
|
destinations: Array<MessageEventBusDestinationOptions>,
|
||||||
disableLicense: false,
|
disableLicense: false,
|
||||||
allDestinations: [] as MessageEventBusDestinationOptions[],
|
allDestinations: [] as MessageEventBusDestinationOptions[],
|
||||||
isInstanceOwner: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (!this.isLicensed) return;
|
if (!this.isLicensed) return;
|
||||||
|
|
||||||
this.isInstanceOwner = this.usersStore.currentUser?.globalRole?.name === 'owner';
|
|
||||||
// Prepare credentialsStore so modals can pick up credentials
|
// Prepare credentialsStore so modals can pick up credentials
|
||||||
await this.credentialsStore.fetchCredentialTypes(false);
|
await this.credentialsStore.fetchCredentialTypes(false);
|
||||||
await this.credentialsStore.fetchAllCredentials();
|
await this.credentialsStore.fetchAllCredentials();
|
||||||
|
@ -141,7 +139,6 @@ export default defineComponent({
|
||||||
useLogStreamingStore,
|
useLogStreamingStore,
|
||||||
useWorkflowsStore,
|
useWorkflowsStore,
|
||||||
useUIStore,
|
useUIStore,
|
||||||
useUsersStore,
|
|
||||||
useCredentialsStore,
|
useCredentialsStore,
|
||||||
),
|
),
|
||||||
sortedItemKeysByLabel() {
|
sortedItemKeysByLabel() {
|
||||||
|
@ -158,6 +155,9 @@ export default defineComponent({
|
||||||
if (this.disableLicense) return false;
|
if (this.disableLicense) return false;
|
||||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.LogStreaming);
|
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.LogStreaming);
|
||||||
},
|
},
|
||||||
|
canManageLogStreaming(): boolean {
|
||||||
|
return hasPermission(['rbac'], { rbac: { scope: 'logStreaming:manage' } });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDestinationWasSaved() {
|
onDestinationWasSaved() {
|
||||||
|
|
Loading…
Reference in a new issue