feat(editor): Add routing middleware, permission checks, RBAC store, RBAC component (#7702)

Github issue / Community forum post (link here to close automatically):

---------

Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
This commit is contained in:
Alex Grozav
2023-11-23 13:22:47 +02:00
committed by GitHub
parent fdb2c18ecc
commit 67a88914f2
62 changed files with 1935 additions and 646 deletions

View File

@@ -122,6 +122,8 @@ import {
import { isNavigationFailure } from 'vue-router';
import ExecutionsUsage from '@/components/ExecutionsUsage.vue';
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
import { ROLE } from '@/utils';
import { hasPermission } from '@/rbac/permissions';
export default defineComponent({
name: 'MainSidebar',
@@ -177,7 +179,9 @@ export default defineComponent({
return accessibleRoute !== null;
},
showUserArea(): boolean {
return this.usersStore.canUserAccessSidebarUserInfo && this.usersStore.currentUser !== null;
return hasPermission(['role'], {
role: [ROLE.Member, ROLE.Owner],
});
},
workflowExecution(): IExecutionResponse | null {
return this.workflowsStore.getWorkflowExecution;
@@ -347,7 +351,7 @@ export default defineComponent({
};
},
},
mounted() {
async mounted() {
this.basePath = this.rootStore.baseUrl;
if (this.$refs.user) {
void this.$externalHooks().run('mainSidebar.mounted', {

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import type { PropType } from 'vue';
import { computed, defineComponent } from 'vue';
import { useRBACStore } from '@/stores/rbac.store';
import type { HasScopeMode, Scope, Resource } from '@n8n/permissions';
import {
inferProjectIdFromRoute,
inferResourceIdFromRoute,
inferResourceTypeFromRoute,
} from '@/utils/rbacUtils';
import { useRoute } from 'vue-router';
export default defineComponent({
props: {
scope: {
type: [String, Array] as PropType<Scope | Scope[]>,
required: true,
},
mode: {
type: String as PropType<HasScopeMode>,
default: 'allOf',
},
resourceType: {
type: String as PropType<Resource>,
default: undefined,
},
resourceId: {
type: String,
default: undefined,
},
projectId: {
type: String,
default: undefined,
},
},
setup(props, { slots }) {
const rbacStore = useRBACStore();
const route = useRoute();
const hasScope = computed(() => {
const projectId = props.projectId ?? inferProjectIdFromRoute(route);
const resourceType = props.resourceType ?? inferResourceTypeFromRoute(route);
const resourceId = resourceType
? props.resourceId ?? inferResourceIdFromRoute(route)
: undefined;
return rbacStore.hasScope(
props.scope,
{
projectId,
resourceType,
resourceId,
},
{ mode: props.mode },
);
});
return () => (hasScope.value ? slots.default?.() : slots.fallback?.());
},
});
</script>

View File

@@ -30,6 +30,7 @@ import TagsTableHeader from '@/components/TagsManager/TagsView/TagsTableHeader.v
import TagsTable from '@/components/TagsManager/TagsView/TagsTable.vue';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { useRBACStore } from '@/stores/rbac.store';
const matches = (name: string, filter: string) =>
name.toLowerCase().trim().includes(filter.toLowerCase().trim());
@@ -50,7 +51,7 @@ export default defineComponent({
};
},
computed: {
...mapStores(useUsersStore),
...mapStores(useUsersStore, useRBACStore),
isCreateEnabled(): boolean {
return (this.tags || []).length === 0 || this.createEnabled;
},
@@ -70,7 +71,7 @@ export default defineComponent({
disable: disabled && tag.id !== this.deleteId && tag.id !== this.updateId,
update: disabled && tag.id === this.updateId,
delete: disabled && tag.id === this.deleteId,
canDelete: this.usersStore.canUserDeleteTags,
canDelete: this.rbacStore.hasScope('tag:delete'),
}),
);

View File

@@ -0,0 +1,50 @@
import RBAC from '@/components/RBAC.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { useRBACStore } from '@/stores/rbac.store';
const renderComponent = createComponentRenderer(RBAC);
vi.mock('vue-router', () => ({
useRoute: vi.fn(() => ({
path: '/workflows',
params: {},
})),
}));
vi.mock('@/stores/rbac.store', () => ({
useRBACStore: vi.fn(),
}));
describe('RBAC', () => {
it('renders default slot when hasScope is true', async () => {
vi.mocked(useRBACStore).mockImplementation(() => ({
hasScope: () => true,
}));
const wrapper = renderComponent({
props: { scope: 'worfklow:list' },
slots: {
default: 'Default Content',
fallback: 'Fallback Content',
},
});
expect(wrapper.getByText('Default Content')).toBeInTheDocument();
});
it('renders fallback slot when hasScope is false', async () => {
vi.mocked(useRBACStore).mockImplementation(() => ({
hasScope: () => false,
}));
const wrapper = renderComponent({
props: { scope: 'worfklow:list' },
slots: {
default: 'Default Content',
fallback: 'Fallback Content',
},
});
expect(wrapper.getByText('Fallback Content')).toBeInTheDocument();
});
});