feat: RBAC (#8922)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Val <68596159+valya@users.noreply.github.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
Co-authored-by: Valya Bullions <valya@n8n.io>
Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: Danny Martini <despair.blue@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: oleg <me@olegivaniv.com>
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
Co-authored-by: Elias Meire <elias@meire.dev>
Co-authored-by: Giulio Andreini <andreini@netseven.it>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: Ayato Hayashi <go12limchangyong@gmail.com>
This commit is contained in:
Csaba Tuncsik
2024-05-17 10:53:15 +02:00
committed by GitHub
parent b1f977ebd0
commit 596c472ecc
292 changed files with 14129 additions and 3989 deletions

View File

@@ -20,21 +20,12 @@
@click:button="goToUpgrade"
/>
</div>
<div v-else-if="isDefaultUser">
<n8n-action-box
:heading="$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.title')"
:description="
$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.description')
"
:button-text="$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.button')"
@click:button="goToUsersSettings"
/>
</div>
<div v-else>
<n8n-info-tip v-if="credentialPermissions.isOwner" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
</n8n-info-tip>
<n8n-info-tip v-if="!credentialPermissions.share" :bold="false" class="mb-s">
<n8n-info-tip
v-if="!credentialPermissions.share && !isHomeTeamProject"
:bold="false"
class="mb-s"
>
{{
$locale.baseText('credentialEdit.credentialSharing.info.sharee', {
interpolate: { credentialOwnerName },
@@ -42,48 +33,48 @@
}}
</n8n-info-tip>
<n8n-info-tip
v-if="
credentialPermissions.read &&
credentialPermissions.share &&
!credentialPermissions.isOwner
"
class="mb-s"
v-if="credentialPermissions.share && !isHomeTeamProject"
:bold="false"
class="mb-s"
>
<i18n-t keypath="credentialEdit.credentialSharing.info.reader">
<template v-if="!isCredentialSharedWithCurrentUser" #notShared>
{{ $locale.baseText('credentialEdit.credentialSharing.info.notShared') }}
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
</n8n-info-tip>
<ProjectSharing
v-model="sharedWithProjects"
:projects="projects"
:roles="credentialRoles"
:home-project="homeProject"
:readonly="!credentialPermissions.share"
:static="isHomeTeamProject || !credentialPermissions.share"
:placeholder="$locale.baseText('workflows.shareModal.select.placeholder')"
/>
<n8n-info-tip v-if="isHomeTeamProject" :bold="false" class="mt-s">
<i18n-t keypath="credentials.shareModal.info.members" tag="span">
<template #projectName>
{{ homeProject?.name }}
</template>
<template #members>
<strong>
{{
$locale.baseText('credentials.shareModal.info.members.number', {
interpolate: {
number: String(numberOfMembersInHomeTeamProject),
},
adjustToNumber: numberOfMembersInHomeTeamProject,
})
}}
</strong>
</template>
</i18n-t>
</n8n-info-tip>
<n8n-user-select
v-if="credentialPermissions.share"
class="mb-s"
size="large"
:users="usersList"
:current-user-id="usersStore.currentUser.id"
:placeholder="$locale.baseText('credentialEdit.credentialSharing.select.placeholder')"
data-test-id="credential-sharing-modal-users-select"
@update:model-value="onAddSharee"
>
<template #prefix>
<n8n-icon icon="search" />
</template>
</n8n-user-select>
<n8n-users-list
:actions="usersListActions"
:users="sharedWithList"
:current-user-id="usersStore.currentUser.id"
:readonly="!credentialPermissions.share"
@delete="onRemoveSharee"
/>
</div>
</div>
</template>
<script lang="ts">
import type { IUser, IUserListAction } from '@/Interface';
import type { ICredentialsResponse, IUser, IUserListAction } from '@/Interface';
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { useMessage } from '@/composables/useMessage';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
@@ -91,25 +82,70 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useUsageStore } from '@/stores/usage.store';
import { EnterpriseEditionFeature, MODAL_CONFIRM, VIEWS } from '@/constants';
import { EnterpriseEditionFeature } from '@/constants';
import ProjectSharing from '@/features/projects/components/ProjectSharing.vue';
import { useProjectsStore } from '@/features/projects/projects.store';
import type {
ProjectListItem,
ProjectSharingData,
Project,
} from '@/features/projects/projects.types';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import type { PermissionsMap } from '@/permissions';
import type { CredentialScope } from '@n8n/permissions';
import type { EventBus } from 'n8n-design-system/utils';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
export default defineComponent({
name: 'CredentialSharing',
props: [
'credential',
'credentialId',
'credentialData',
'sharedWith',
'credentialPermissions',
'modalBus',
],
components: {
ProjectSharing,
},
props: {
credential: {
type: Object as PropType<ICredentialsResponse>,
required: true,
},
credentialId: {
type: String,
required: true,
},
credentialData: {
type: Object as PropType<ICredentialDataDecryptedObject>,
required: true,
},
credentialPermissions: {
type: Object as PropType<PermissionsMap<CredentialScope>>,
required: true,
},
modalBus: {
type: Object as PropType<EventBus>,
required: true,
},
},
emits: ['update:modelValue'],
setup() {
return {
...useMessage(),
};
},
data() {
return {
sharedWithProjects: [...(this.credential?.sharedWithProjects ?? [])] as ProjectSharingData[],
teamProject: null as Project | null,
};
},
computed: {
...mapStores(useCredentialsStore, useUsersStore, useUsageStore, useUIStore, useSettingsStore),
...mapStores(
useCredentialsStore,
useUsersStore,
useUsageStore,
useUIStore,
useSettingsStore,
useProjectsStore,
useRolesStore,
),
usersListActions(): IUserListAction[] {
return [
{
@@ -118,83 +154,65 @@ export default defineComponent({
},
];
},
isDefaultUser(): boolean {
return this.usersStore.isDefaultUser;
},
isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
},
usersList(): IUser[] {
return this.usersStore.allUsers.filter((user: IUser) => {
const isAlreadySharedWithUser = (this.credentialData.sharedWith || []).find(
(sharee: IUser) => sharee.id === user.id,
);
const isOwner = this.credentialData.ownedBy?.id === user.id;
return !isAlreadySharedWithUser && !isOwner;
});
},
sharedWithList(): IUser[] {
return [
{
...(this.credential ? this.credential.ownedBy : this.usersStore.currentUser),
isOwner: true,
},
].concat(this.credentialData.sharedWith || []);
},
credentialOwnerName(): string {
return this.credentialsStore.getCredentialOwnerNameById(`${this.credentialId}`);
},
isCredentialSharedWithCurrentUser(): boolean {
return (this.credentialData.sharedWith || []).some((sharee: IUser) => {
return (this.credentialData.sharedWithProjects || []).some((sharee: IUser) => {
return sharee.id === this.usersStore.currentUser?.id;
});
},
projects(): ProjectListItem[] {
return this.projectsStore.personalProjects.filter(
(project) =>
project.id !== this.credential?.homeProject?.id &&
project.id !== this.credentialData?.homeProject?.id,
);
},
homeProject(): ProjectSharingData | undefined {
return (
this.credential?.homeProject ?? (this.credentialData?.homeProject as ProjectSharingData)
);
},
isHomeTeamProject(): boolean {
return this.homeProject?.type === 'team';
},
numberOfMembersInHomeTeamProject(): number {
return this.teamProject?.relations.length ?? 0;
},
credentialRoleTranslations(): Record<string, string> {
return {
'credential:user': this.$locale.baseText('credentialEdit.credentialSharing.role.user'),
};
},
credentialRoles(): RoleMap['credential'] {
return this.rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
role,
name: this.credentialRoleTranslations[role],
scopes,
licensed,
}));
},
},
mounted() {
void this.loadUsers();
watch: {
sharedWithProjects: {
handler(changedSharedWithProjects: ProjectSharingData[]) {
this.$emit('update:modelValue', changedSharedWithProjects);
},
deep: true,
},
},
async mounted() {
await Promise.all([this.usersStore.fetchUsers(), this.projectsStore.getAllProjects()]);
if (this.homeProject && this.isHomeTeamProject) {
this.teamProject = await this.projectsStore.fetchProject(this.homeProject.id);
}
},
methods: {
async onAddSharee(userId: string) {
const sharee = { ...this.usersStore.getUserById(userId), isOwner: false };
this.$emit('update:modelValue', (this.credentialData.sharedWith || []).concat(sharee));
},
async onRemoveSharee(userId: string) {
const user = this.usersStore.getUserById(userId);
if (user) {
const confirm = await this.confirm(
this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.message', {
interpolate: { name: user.fullName || '' },
}),
this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.title'),
{
confirmButtonText: this.$locale.baseText(
'credentialEdit.credentialSharing.list.delete.confirm.confirmButtonText',
),
cancelButtonText: this.$locale.baseText(
'credentialEdit.credentialSharing.list.delete.confirm.cancelButtonText',
),
},
);
if (confirm === MODAL_CONFIRM) {
this.$emit(
'update:modelValue',
this.credentialData.sharedWith.filter((sharee: IUser) => {
return sharee.id !== user.id;
}),
);
}
}
},
async loadUsers() {
await this.usersStore.fetchUsers();
},
goToUsersSettings() {
void this.$router.push({ name: VIEWS.USERS_SETTINGS });
this.modalBus.emit('close');
},
goToUpgrade() {
void this.uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
},