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:
@@ -6,7 +6,7 @@
|
||||
:message="
|
||||
$locale.baseText(
|
||||
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
|
||||
credentialPermissions.update || credentialPermissions.isOwner ? '' : '.sharee'
|
||||
credentialPermissions.update ? '' : '.sharee'
|
||||
}`,
|
||||
{ interpolate: { owner: credentialOwnerName } },
|
||||
)
|
||||
@@ -19,7 +19,7 @@
|
||||
:message="
|
||||
$locale.baseText(
|
||||
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
|
||||
credentialPermissions.update || credentialPermissions.isOwner ? '' : '.sharee'
|
||||
credentialPermissions.update ? '' : '.sharee'
|
||||
}`,
|
||||
{ interpolate: { owner: credentialOwnerName } },
|
||||
)
|
||||
@@ -114,10 +114,7 @@
|
||||
|
||||
<OauthButton
|
||||
v-if="
|
||||
isOAuthType &&
|
||||
requiredPropertiesFilled &&
|
||||
!isOAuthConnected &&
|
||||
credentialPermissions.isOwner
|
||||
isOAuthType && requiredPropertiesFilled && !isOAuthConnected && credentialPermissions.update
|
||||
"
|
||||
:is-google-o-auth-type="isGoogleOAuthType"
|
||||
@click="$emit('oauth')"
|
||||
@@ -142,6 +139,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
|
||||
import type { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
|
||||
@@ -153,7 +151,8 @@ import CredentialInputs from './CredentialInputs.vue';
|
||||
import OauthButton from './OauthButton.vue';
|
||||
import { addCredentialTranslation } from '@/plugins/i18n';
|
||||
import { BUILTIN_CREDENTIALS_DOCS_URL, DOCS_DOMAIN, EnterpriseEditionFeature } from '@/constants';
|
||||
import type { IPermissions } from '@/permissions';
|
||||
import type { PermissionsMap } from '@/permissions';
|
||||
import type { CredentialScope } from '@n8n/permissions';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
@@ -214,8 +213,8 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
},
|
||||
credentialPermissions: {
|
||||
type: Object,
|
||||
default: (): IPermissions => ({}),
|
||||
type: Object as PropType<PermissionsMap<CredentialScope>>,
|
||||
default: () => ({}) as PermissionsMap<CredentialScope>,
|
||||
},
|
||||
requiredPropertiesFilled: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
@click="deleteCredential"
|
||||
/>
|
||||
<SaveButton
|
||||
v-if="(hasUnsavedChanges || credentialId) && credentialPermissions.save"
|
||||
v-if="showSaveButton"
|
||||
:saved="!hasUnsavedChanges && !isTesting"
|
||||
:is-saving="isSaving || isTesting"
|
||||
:saving-label="
|
||||
@@ -86,7 +86,7 @@
|
||||
@auth-type-changed="onAuthTypeChanged"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="activeTab === 'sharing' && credentialType" :class="$style.mainContent">
|
||||
<div v-else-if="showSharingContent" :class="$style.mainContent">
|
||||
<CredentialSharing
|
||||
:credential="currentCredential"
|
||||
:credential-data="credentialData"
|
||||
@@ -127,7 +127,6 @@ import type {
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
ITelemetryTrackProperties,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeHelpers } from 'n8n-workflow';
|
||||
import CredentialIcon from '@/components/CredentialIcon.vue';
|
||||
@@ -143,7 +142,7 @@ import Modal from '@/components/Modal.vue';
|
||||
import InlineNameEdit from '@/components/InlineNameEdit.vue';
|
||||
import { CREDENTIAL_EDIT_MODAL_KEY, EnterpriseEditionFeature, MODAL_CONFIRM } from '@/constants';
|
||||
import FeatureComingSoon from '@/components/FeatureComingSoon.vue';
|
||||
import type { IPermissions } from '@/permissions';
|
||||
import type { PermissionsMap } from '@/permissions';
|
||||
import { getCredentialPermissions } from '@/permissions';
|
||||
import type { IMenuItem } from 'n8n-design-system';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
@@ -154,6 +153,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import type { ProjectSharingData } from '@/features/projects/projects.types';
|
||||
|
||||
import {
|
||||
getNodeAuthOptions,
|
||||
@@ -163,6 +163,8 @@ import {
|
||||
import { isValidCredentialResponse, isCredentialModalState } from '@/utils/typeGuards';
|
||||
import { isExpression, isTestableExpression } from '@/utils/expressions';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useProjectsStore } from '@/features/projects/projects.store';
|
||||
import type { CredentialScope } from '@n8n/permissions';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CredentialEdit',
|
||||
@@ -222,65 +224,6 @@ export default defineComponent({
|
||||
isSharedWithChanged: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.requiredCredentials =
|
||||
isCredentialModalState(this.uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY]) &&
|
||||
this.uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].showAuthSelector === true;
|
||||
|
||||
if (this.mode === 'new' && this.credentialTypeName) {
|
||||
this.credentialName = await this.credentialsStore.getNewCredentialName({
|
||||
credentialTypeName: this.defaultCredentialTypeName,
|
||||
});
|
||||
|
||||
if (this.currentUser) {
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
ownedBy: {
|
||||
id: this.currentUser.id,
|
||||
firstName: this.currentUser.firstName,
|
||||
lastName: this.currentUser.lastName,
|
||||
email: this.currentUser.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
await this.loadCurrentCredential();
|
||||
}
|
||||
|
||||
if (this.credentialType) {
|
||||
for (const property of this.credentialType.properties) {
|
||||
if (
|
||||
!this.credentialData.hasOwnProperty(property.name) &&
|
||||
!this.credentialType.__overwrittenProperties?.includes(property.name)
|
||||
) {
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
[property.name]: property.default as CredentialInformation,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.externalHooks.run('credentialsEdit.credentialModalOpened', {
|
||||
credentialType: this.credentialTypeName,
|
||||
isEditingCredential: this.mode === 'edit',
|
||||
activeNode: this.ndvStore.activeNode,
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
if (this.credentialId) {
|
||||
if (!this.requiredPropertiesFilled && this.credentialPermissions.isOwner) {
|
||||
// sharees can't see properties, so this check would always fail for them
|
||||
// if the credential contains required fields.
|
||||
this.showValidationWarning = true;
|
||||
} else {
|
||||
await this.retestCredential();
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useCredentialsStore,
|
||||
@@ -290,6 +233,7 @@ export default defineComponent({
|
||||
useUsersStore,
|
||||
useWorkflowsStore,
|
||||
useNodeTypesStore,
|
||||
useProjectsStore,
|
||||
),
|
||||
activeNodeType(): INodeTypeDescription | null {
|
||||
const activeNode = this.ndvStore.activeNode;
|
||||
@@ -363,16 +307,11 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
isCredentialTestable(): boolean {
|
||||
// Sharees can always test since they can't see the data.
|
||||
if (!this.credentialPermissions.isOwner) {
|
||||
return true;
|
||||
}
|
||||
if (this.isOAuthType || !this.requiredPropertiesFilled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { ownedBy, sharedWith, ...credentialData } = this.credentialData;
|
||||
const hasUntestableExpressions = Object.values(credentialData).reduce(
|
||||
const hasUntestableExpressions = Object.values(this.credentialData).reduce(
|
||||
(accu: boolean, value: CredentialInformation) =>
|
||||
accu ||
|
||||
(typeof value === 'string' && isExpression(value) && !isTestableExpression(value)),
|
||||
@@ -480,18 +419,17 @@ export default defineComponent({
|
||||
}
|
||||
return true;
|
||||
},
|
||||
credentialPermissions(): IPermissions {
|
||||
credentialPermissions(): PermissionsMap<CredentialScope> {
|
||||
if (this.loading) {
|
||||
return {};
|
||||
return {} as PermissionsMap<CredentialScope>;
|
||||
}
|
||||
|
||||
return getCredentialPermissions(
|
||||
this.currentUser,
|
||||
(this.credentialId ? this.currentCredential : this.credentialData) as ICredentialsResponse,
|
||||
);
|
||||
},
|
||||
sidebarItems(): IMenuItem[] {
|
||||
return [
|
||||
const menuItems: IMenuItem[] = [
|
||||
{
|
||||
id: 'connection',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.connection'),
|
||||
@@ -508,6 +446,8 @@ export default defineComponent({
|
||||
position: 'top',
|
||||
},
|
||||
];
|
||||
|
||||
return menuItems;
|
||||
},
|
||||
isSharingAvailable(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
|
||||
@@ -521,6 +461,75 @@ export default defineComponent({
|
||||
}
|
||||
return credentialTypeName || '';
|
||||
},
|
||||
showSaveButton(): boolean {
|
||||
return (
|
||||
(this.hasUnsavedChanges || !!this.credentialId) &&
|
||||
(this.credentialPermissions.create || this.credentialPermissions.update)
|
||||
);
|
||||
},
|
||||
showSharingMenu(): boolean {
|
||||
return !this.$route.params.projectId;
|
||||
},
|
||||
showSharingContent(): boolean {
|
||||
return this.activeTab === 'sharing' && !!this.credentialType;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.requiredCredentials =
|
||||
isCredentialModalState(this.uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY]) &&
|
||||
this.uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].showAuthSelector === true;
|
||||
|
||||
if (this.mode === 'new' && this.credentialTypeName) {
|
||||
this.credentialName = await this.credentialsStore.getNewCredentialName({
|
||||
credentialTypeName: this.defaultCredentialTypeName,
|
||||
});
|
||||
|
||||
const { currentProject, personalProject } = this.projectsStore;
|
||||
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
|
||||
const homeProject = currentProject ?? personalProject ?? {};
|
||||
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
scopes,
|
||||
homeProject,
|
||||
};
|
||||
} else {
|
||||
await this.loadCurrentCredential();
|
||||
}
|
||||
|
||||
if (this.credentialType) {
|
||||
for (const property of this.credentialType.properties) {
|
||||
if (
|
||||
!this.credentialData.hasOwnProperty(property.name) &&
|
||||
!this.credentialType.__overwrittenProperties?.includes(property.name)
|
||||
) {
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
[property.name]: property.default as CredentialInformation,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.externalHooks.run('credentialsEdit.credentialModalOpened', {
|
||||
credentialType: this.credentialTypeName,
|
||||
isEditingCredential: this.mode === 'edit',
|
||||
activeNode: this.ndvStore.activeNode,
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
if (this.credentialId) {
|
||||
if (!this.requiredPropertiesFilled && this.credentialPermissions.update) {
|
||||
// sharees can't see properties, so this check would always fail for them
|
||||
// if the credential contains required fields.
|
||||
this.showValidationWarning = true;
|
||||
} else {
|
||||
await this.retestCredential();
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
methods: {
|
||||
async beforeClose() {
|
||||
@@ -546,7 +555,7 @@ export default defineComponent({
|
||||
},
|
||||
);
|
||||
keepEditing = confirmAction === MODAL_CONFIRM;
|
||||
} else if (this.credentialPermissions.isOwner && this.isOAuthType && !this.isOAuthConnected) {
|
||||
} else if (this.isOAuthType && !this.isOAuthConnected) {
|
||||
const confirmAction = await this.confirm(
|
||||
this.$locale.baseText(
|
||||
'credentialEdit.credentialEdit.confirmMessage.beforeClose2.message',
|
||||
@@ -622,12 +631,12 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async loadCurrentCredential() {
|
||||
this.credentialId = this.activeId;
|
||||
this.credentialId = (this.activeId ?? '') as string;
|
||||
|
||||
try {
|
||||
const currentCredentials = await this.credentialsStore.getCredentialData({
|
||||
const currentCredentials = (await this.credentialsStore.getCredentialData({
|
||||
id: this.credentialId,
|
||||
});
|
||||
})) as unknown as ICredentialDataDecryptedObject;
|
||||
|
||||
if (!currentCredentials) {
|
||||
throw new Error(
|
||||
@@ -638,20 +647,20 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
this.credentialData = (currentCredentials.data as ICredentialDataDecryptedObject) || {};
|
||||
if (currentCredentials.sharedWith) {
|
||||
if (currentCredentials.sharedWithProjects) {
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
sharedWith: currentCredentials.sharedWith as IDataObject[],
|
||||
sharedWithProjects: currentCredentials.sharedWithProjects,
|
||||
};
|
||||
}
|
||||
if (currentCredentials.ownedBy) {
|
||||
if (currentCredentials.homeProject) {
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
ownedBy: currentCredentials.ownedBy as IDataObject[],
|
||||
homeProject: currentCredentials.homeProject,
|
||||
};
|
||||
}
|
||||
|
||||
this.credentialName = currentCredentials.name;
|
||||
this.credentialName = currentCredentials.name as string;
|
||||
} catch (error) {
|
||||
this.showError(
|
||||
error,
|
||||
@@ -677,10 +686,10 @@ export default defineComponent({
|
||||
sharing_enabled: EnterpriseEditionFeature.Sharing,
|
||||
});
|
||||
},
|
||||
onChangeSharedWith(sharees: IDataObject[]) {
|
||||
onChangeSharedWith(sharedWithProjects: ProjectSharingData[]) {
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
sharedWith: sharees,
|
||||
sharedWithProjects,
|
||||
};
|
||||
this.isSharedWithChanged = true;
|
||||
this.hasUnsavedChanges = true;
|
||||
@@ -751,7 +760,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
const { ownedBy, sharedWith, ...credentialData } = this.credentialData;
|
||||
const { ownedBy, sharedWithProjects, ...credentialData } = this.credentialData;
|
||||
const details: ICredentialsDecrypted = {
|
||||
id: this.credentialId,
|
||||
name: this.credentialName,
|
||||
@@ -796,29 +805,60 @@ export default defineComponent({
|
||||
null,
|
||||
);
|
||||
|
||||
let sharedWith: IUser[] | undefined;
|
||||
let ownedBy: IUser | undefined;
|
||||
if (this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
|
||||
sharedWith = this.credentialData.sharedWith as unknown as IUser[];
|
||||
ownedBy = this.credentialData.ownedBy as unknown as IUser;
|
||||
}
|
||||
|
||||
const credentialDetails: ICredentialsDecrypted = {
|
||||
id: this.credentialId,
|
||||
name: this.credentialName,
|
||||
type: this.credentialTypeName!,
|
||||
data: data as unknown as ICredentialDataDecryptedObject,
|
||||
sharedWith,
|
||||
ownedBy,
|
||||
nodesAccess: [],
|
||||
};
|
||||
|
||||
if (
|
||||
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) &&
|
||||
this.credentialData.sharedWithProjects
|
||||
) {
|
||||
credentialDetails.sharedWithProjects = this.credentialData
|
||||
.sharedWithProjects as ProjectSharingData[];
|
||||
}
|
||||
|
||||
let credential;
|
||||
|
||||
const isNewCredential = this.mode === 'new' && !this.credentialId;
|
||||
|
||||
if (isNewCredential) {
|
||||
credential = await this.createCredential(credentialDetails);
|
||||
credential = await this.createCredential(
|
||||
credentialDetails,
|
||||
this.projectsStore.currentProjectId,
|
||||
);
|
||||
|
||||
let toastTitle = this.$locale.baseText('credentials.create.personal.toast.title');
|
||||
let toastText = '';
|
||||
|
||||
if (!credentialDetails.sharedWithProjects) {
|
||||
toastText = this.$locale.baseText('credentials.create.personal.toast.text');
|
||||
}
|
||||
|
||||
if (this.projectsStore.currentProject) {
|
||||
toastTitle = this.$locale.baseText('credentials.create.project.toast.title', {
|
||||
interpolate: { projectName: this.projectsStore.currentProject.name ?? '' },
|
||||
});
|
||||
|
||||
toastText = this.$locale.baseText('credentials.create.project.toast.text', {
|
||||
interpolate: { projectName: this.projectsStore.currentProject.name ?? '' },
|
||||
});
|
||||
}
|
||||
|
||||
this.showMessage({
|
||||
title: toastTitle,
|
||||
message: toastText,
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
if (this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
|
||||
credentialDetails.sharedWithProjects = this.credentialData
|
||||
.sharedWithProjects as ProjectSharingData[];
|
||||
}
|
||||
|
||||
credential = await this.updateCredential(credentialDetails);
|
||||
}
|
||||
|
||||
@@ -876,11 +916,12 @@ export default defineComponent({
|
||||
|
||||
async createCredential(
|
||||
credentialDetails: ICredentialsDecrypted,
|
||||
projectId?: string,
|
||||
): Promise<ICredentialsResponse | null> {
|
||||
let credential;
|
||||
|
||||
try {
|
||||
credential = await this.credentialsStore.createNewCredential(credentialDetails);
|
||||
credential = await this.credentialsStore.createNewCredential(credentialDetails, projectId);
|
||||
this.hasUnsavedChanges = false;
|
||||
} catch (error) {
|
||||
this.showError(
|
||||
@@ -920,11 +961,11 @@ export default defineComponent({
|
||||
if (
|
||||
this.credentialPermissions.share &&
|
||||
this.isSharedWithChanged &&
|
||||
credentialDetails.sharedWith
|
||||
credentialDetails.sharedWithProjects
|
||||
) {
|
||||
credential = await this.credentialsStore.setCredentialSharedWith({
|
||||
credentialId: credentialDetails.id,
|
||||
sharedWith: credentialDetails.sharedWith,
|
||||
sharedWithProjects: credentialDetails.sharedWithProjects,
|
||||
});
|
||||
this.isSharedWithChanged = false;
|
||||
}
|
||||
@@ -1104,6 +1145,16 @@ export default defineComponent({
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const { currentProject, personalProject } = this.projectsStore;
|
||||
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
|
||||
const homeProject = currentProject ?? personalProject ?? {};
|
||||
|
||||
this.credentialData = {
|
||||
...this.credentialData,
|
||||
scopes,
|
||||
homeProject,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user