feat: Introduce advanced permissions (#7844)
This PR introduces the possibility of inviting new users with an `admin` role and changing the role of already invited users. Also using scoped permission checks where applicable instead of using user role checks. --------- Co-authored-by: Val <68596159+valya@users.noreply.github.com> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
@@ -61,7 +61,7 @@
|
||||
data-test-id="credentials-config-container-test-success"
|
||||
/>
|
||||
|
||||
<template v-if="credentialPermissions.updateConnection">
|
||||
<template v-if="credentialPermissions.update">
|
||||
<n8n-notice v-if="documentationUrl && credentialProperties.length" theme="warning">
|
||||
{{ $locale.baseText('credentialEdit.credentialConfig.needHelpFillingOutTheseFields') }}
|
||||
<span class="ml-4xs">
|
||||
@@ -104,7 +104,7 @@
|
||||
</enterprise-edition>
|
||||
|
||||
<CredentialInputs
|
||||
v-if="credentialType && credentialPermissions.updateConnection"
|
||||
v-if="credentialType && credentialPermissions.update"
|
||||
:credentialData="credentialData"
|
||||
:credentialProperties="credentialProperties"
|
||||
:documentationUrl="documentationUrl"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<InlineNameEdit
|
||||
:modelValue="credentialName"
|
||||
:subtitle="credentialType ? credentialType.displayName : ''"
|
||||
:readonly="!credentialPermissions.updateName || !credentialType"
|
||||
:readonly="!credentialPermissions.update || !credentialType"
|
||||
type="Credential"
|
||||
@update:modelValue="onNameEdit"
|
||||
data-test-id="credential-name"
|
||||
@@ -224,6 +224,7 @@ export default defineComponent({
|
||||
selectedCredential: '',
|
||||
requiredCredentials: false, // Are credentials required or optional for the node
|
||||
hasUserSpecifiedName: false,
|
||||
isSharedWithChanged: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
@@ -683,6 +684,7 @@ export default defineComponent({
|
||||
...this.credentialData,
|
||||
sharedWith: sharees,
|
||||
};
|
||||
this.isSharedWithChanged = true;
|
||||
this.hasUnsavedChanges = true;
|
||||
},
|
||||
|
||||
@@ -920,10 +922,23 @@ export default defineComponent({
|
||||
): Promise<ICredentialsResponse | null> {
|
||||
let credential;
|
||||
try {
|
||||
credential = await this.credentialsStore.updateCredential({
|
||||
id: this.credentialId,
|
||||
data: credentialDetails,
|
||||
});
|
||||
if (this.credentialPermissions.update) {
|
||||
credential = await this.credentialsStore.updateCredential({
|
||||
id: this.credentialId,
|
||||
data: credentialDetails,
|
||||
});
|
||||
}
|
||||
if (
|
||||
this.credentialPermissions.share &&
|
||||
this.isSharedWithChanged &&
|
||||
credentialDetails.sharedWith
|
||||
) {
|
||||
credential = await this.credentialsStore.setCredentialSharedWith({
|
||||
credentialId: credentialDetails.id,
|
||||
sharedWith: credentialDetails.sharedWith,
|
||||
});
|
||||
this.isSharedWithChanged = false;
|
||||
}
|
||||
this.hasUnsavedChanges = false;
|
||||
} catch (error) {
|
||||
this.showError(
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-col :span="16">
|
||||
<div v-for="node in nodesWithAccess" :key="node.name" :class="$style.valueLabel">
|
||||
<el-checkbox
|
||||
v-if="credentialPermissions.updateNodeAccess"
|
||||
v-if="credentialPermissions.update"
|
||||
:label="
|
||||
$locale.headerText({
|
||||
key: `headers.${shortNodeType(node)}.displayName`,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<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.updateSharing" :bold="false" class="mb-s">
|
||||
<n8n-info-tip v-if="!credentialPermissions.share" :bold="false" class="mb-s">
|
||||
{{
|
||||
$locale.baseText('credentialEdit.credentialSharing.info.sharee', {
|
||||
interpolate: { credentialOwnerName },
|
||||
@@ -42,10 +42,14 @@
|
||||
}}
|
||||
</n8n-info-tip>
|
||||
<n8n-info-tip v-if="credentialPermissions.read" class="mb-s" :bold="false">
|
||||
{{ $locale.baseText('credentialEdit.credentialSharing.info.reader') }}
|
||||
<i18n-t keypath="credentialEdit.credentialSharing.info.reader">
|
||||
<template v-if="!isCredentialSharedWithCurrentUser" #notShared>
|
||||
{{ $locale.baseText('credentialEdit.credentialSharing.info.notShared') }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
</n8n-info-tip>
|
||||
<n8n-user-select
|
||||
v-if="credentialPermissions.updateSharing"
|
||||
v-if="credentialPermissions.share"
|
||||
class="mb-s"
|
||||
size="large"
|
||||
:users="usersList"
|
||||
@@ -62,7 +66,7 @@
|
||||
:actions="usersListActions"
|
||||
:users="sharedWithList"
|
||||
:currentUserId="usersStore.currentUser.id"
|
||||
:readonly="!credentialPermissions.updateSharing"
|
||||
:readonly="!credentialPermissions.share"
|
||||
@delete="onRemoveSharee"
|
||||
/>
|
||||
</div>
|
||||
@@ -114,13 +118,12 @@ export default defineComponent({
|
||||
},
|
||||
usersList(): IUser[] {
|
||||
return this.usersStore.allUsers.filter((user: IUser) => {
|
||||
const isCurrentUser = user.id === this.usersStore.currentUser?.id;
|
||||
const isAlreadySharedWithUser = (this.credentialData.sharedWith || []).find(
|
||||
(sharee: IUser) => sharee.id === user.id,
|
||||
);
|
||||
const isOwner = this.credentialData.ownedBy.id === user.id;
|
||||
const isOwner = this.credentialData.ownedBy?.id === user.id;
|
||||
|
||||
return !isCurrentUser && !isAlreadySharedWithUser && !isOwner;
|
||||
return !isAlreadySharedWithUser && !isOwner;
|
||||
});
|
||||
},
|
||||
sharedWithList(): IUser[] {
|
||||
@@ -134,6 +137,11 @@ export default defineComponent({
|
||||
credentialOwnerName(): string {
|
||||
return this.credentialsStore.getCredentialOwnerNameById(`${this.credentialId}`);
|
||||
},
|
||||
isCredentialSharedWithCurrentUser(): boolean {
|
||||
return (this.credentialData.sharedWith || []).some((sharee: IUser) => {
|
||||
return sharee.id === this.usersStore.currentUser?.id;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onAddSharee(userId: string) {
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
:eventBus="modalBus"
|
||||
>
|
||||
<template #content>
|
||||
<n8n-notice v-if="!isAdvancedPermissionsEnabled">
|
||||
<i18n-t keypath="settings.users.advancedPermissions.warning">
|
||||
<template #link>
|
||||
<n8n-link size="small" @click="goToUpgradeAdvancedPermissions">
|
||||
{{ $locale.baseText('settings.users.advancedPermissions.warning.link') }}
|
||||
</n8n-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</n8n-notice>
|
||||
<div v-if="showInviteUrls">
|
||||
<n8n-users-list :users="invitedUsers">
|
||||
<template #actions="{ user }">
|
||||
@@ -58,10 +67,11 @@ import { useToast } from '@/composables/useToast';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import Modal from './Modal.vue';
|
||||
import type { IFormInputs, IInviteResponse, IUser } from '@/Interface';
|
||||
import { VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
|
||||
import { ROLE } from '@/utils/userUtils';
|
||||
import { EnterpriseEditionFeature, VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
|
||||
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;
|
||||
@@ -97,6 +107,7 @@ export default defineComponent({
|
||||
formBus: createEventBus(),
|
||||
modalBus: createEventBus(),
|
||||
emails: '',
|
||||
role: 'member',
|
||||
showInviteUrls: null as IInviteResponse[] | null,
|
||||
loading: false,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
@@ -132,6 +143,11 @@ export default defineComponent({
|
||||
value: ROLE.Member,
|
||||
label: this.$locale.baseText('auth.roles.member'),
|
||||
},
|
||||
{
|
||||
value: ROLE.Admin,
|
||||
label: this.$locale.baseText('auth.roles.admin'),
|
||||
disabled: !this.isAdvancedPermissionsEnabled,
|
||||
},
|
||||
],
|
||||
capitalize: true,
|
||||
},
|
||||
@@ -139,7 +155,7 @@ export default defineComponent({
|
||||
];
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUsersStore, useSettingsStore),
|
||||
...mapStores(useUsersStore, useSettingsStore, useUIStore),
|
||||
emailsCount(): number {
|
||||
return this.emails.split(',').filter((email: string) => !!email.trim()).length;
|
||||
},
|
||||
@@ -167,6 +183,11 @@ export default defineComponent({
|
||||
)
|
||||
: [];
|
||||
},
|
||||
isAdvancedPermissionsEnabled(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(
|
||||
EnterpriseEditionFeature.AdvancedPermissions,
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
validateEmails(value: string | number | boolean | null | undefined) {
|
||||
@@ -193,6 +214,9 @@ export default defineComponent({
|
||||
if (e.name === 'emails') {
|
||||
this.emails = e.value;
|
||||
}
|
||||
if (e.name === 'role') {
|
||||
this.role = e.value;
|
||||
}
|
||||
},
|
||||
async onSubmit() {
|
||||
try {
|
||||
@@ -200,7 +224,7 @@ export default defineComponent({
|
||||
|
||||
const emails = this.emails
|
||||
.split(',')
|
||||
.map((email) => ({ email: getEmail(email) }))
|
||||
.map((email) => ({ email: getEmail(email), role: this.role }))
|
||||
.filter((invite) => !!invite.email);
|
||||
|
||||
if (emails.length === 0) {
|
||||
@@ -308,6 +332,9 @@ export default defineComponent({
|
||||
this.showCopyInviteLinkToast([]);
|
||||
}
|
||||
},
|
||||
goToUpgradeAdvancedPermissions() {
|
||||
void this.uiStore.goToUpgrade('settings-users', 'upgrade-advanced-permissions');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -210,13 +210,12 @@ export default defineComponent({
|
||||
},
|
||||
usersList(): IUser[] {
|
||||
return this.usersStore.allUsers.filter((user: IUser) => {
|
||||
const isCurrentUser = user.id === this.usersStore.currentUser?.id;
|
||||
const isAlreadySharedWithUser = (this.sharedWith || []).find(
|
||||
(sharee) => sharee.id === user.id,
|
||||
);
|
||||
const isOwner = this.workflow?.ownedBy?.id === user.id;
|
||||
|
||||
return !isCurrentUser && !isAlreadySharedWithUser && !isOwner;
|
||||
return !isAlreadySharedWithUser && !isOwner;
|
||||
});
|
||||
},
|
||||
sharedWithList(): Array<Partial<IUser>> {
|
||||
|
||||
Reference in New Issue
Block a user