mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
fix(editor): Allow owners and admins to share workflows and credentials they don't own (#7833)
This commit is contained in:
parent
1c6178759c
commit
3ab3ec9da8
|
@ -18,7 +18,7 @@ export type WildcardScope = `${Resource}:*` | '*';
|
||||||
export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share'>;
|
export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share'>;
|
||||||
export type TagScope = ResourceScope<'tag'>;
|
export type TagScope = ResourceScope<'tag'>;
|
||||||
export type UserScope = ResourceScope<'user'>;
|
export type UserScope = ResourceScope<'user'>;
|
||||||
export type CredentialScope = ResourceScope<'credential'>;
|
export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share'>;
|
||||||
export type VariableScope = ResourceScope<'variable'>;
|
export type VariableScope = ResourceScope<'variable'>;
|
||||||
export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>;
|
export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>;
|
||||||
export type ExternalSecretStoreScope = ResourceScope<
|
export type ExternalSecretStoreScope = ResourceScope<
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const ownerPermissions: Scope[] = [
|
||||||
'credential:update',
|
'credential:update',
|
||||||
'credential:delete',
|
'credential:delete',
|
||||||
'credential:list',
|
'credential:list',
|
||||||
|
'credential:share',
|
||||||
'variable:create',
|
'variable:create',
|
||||||
'variable:read',
|
'variable:read',
|
||||||
'variable:update',
|
'variable:update',
|
||||||
|
|
|
@ -142,7 +142,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
async onAction(action: string) {
|
async onAction(action: string) {
|
||||||
if (action === CREDENTIAL_LIST_ITEM_ACTIONS.OPEN) {
|
if (action === CREDENTIAL_LIST_ITEM_ACTIONS.OPEN) {
|
||||||
await this.onClick();
|
await this.onClick(new Event('click'));
|
||||||
} else if (action === CREDENTIAL_LIST_ITEM_ACTIONS.DELETE) {
|
} else if (action === CREDENTIAL_LIST_ITEM_ACTIONS.DELETE) {
|
||||||
const deleteConfirmed = await this.confirm(
|
const deleteConfirmed = await this.confirm(
|
||||||
this.$locale.baseText(
|
this.$locale.baseText(
|
||||||
|
|
|
@ -31,28 +31,18 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<n8n-info-tip :bold="false" class="mb-s">
|
<n8n-info-tip v-if="credentialPermissions.isOwner" :bold="false" class="mb-s">
|
||||||
<template v-if="credentialPermissions.isOwner">
|
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
|
||||||
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{
|
|
||||||
$locale.baseText('credentialEdit.credentialSharing.info.sharee', {
|
|
||||||
interpolate: { credentialOwnerName },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</template>
|
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
<n8n-info-tip
|
<n8n-info-tip v-if="!credentialPermissions.updateSharing" :bold="false" class="mb-s">
|
||||||
v-if="
|
{{
|
||||||
!credentialPermissions.isOwner &&
|
$locale.baseText('credentialEdit.credentialSharing.info.sharee', {
|
||||||
!credentialPermissions.isSharee &&
|
interpolate: { credentialOwnerName },
|
||||||
credentialPermissions.isInstanceOwner
|
})
|
||||||
"
|
}}
|
||||||
class="mb-s"
|
</n8n-info-tip>
|
||||||
:bold="false"
|
<n8n-info-tip v-if="credentialPermissions.read" class="mb-s" :bold="false">
|
||||||
>
|
{{ $locale.baseText('credentialEdit.credentialSharing.info.reader') }}
|
||||||
{{ $locale.baseText('credentialEdit.credentialSharing.info.instanceOwner') }}
|
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
<n8n-user-select
|
<n8n-user-select
|
||||||
v-if="credentialPermissions.updateSharing"
|
v-if="credentialPermissions.updateSharing"
|
||||||
|
@ -128,8 +118,9 @@ export default defineComponent({
|
||||||
const isAlreadySharedWithUser = (this.credentialData.sharedWith || []).find(
|
const isAlreadySharedWithUser = (this.credentialData.sharedWith || []).find(
|
||||||
(sharee: IUser) => sharee.id === user.id,
|
(sharee: IUser) => sharee.id === user.id,
|
||||||
);
|
);
|
||||||
|
const isOwner = this.credentialData.ownedBy.id === user.id;
|
||||||
|
|
||||||
return !isCurrentUser && !isAlreadySharedWithUser;
|
return !isCurrentUser && !isAlreadySharedWithUser && !isOwner;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sharedWithList(): IUser[] {
|
sharedWithList(): IUser[] {
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.container">
|
<div v-else :class="$style.container">
|
||||||
<n8n-info-tip v-if="!workflowPermissions.isOwner" :bold="false" class="mb-s">
|
<n8n-info-tip v-if="!workflowPermissions.updateSharing" :bold="false" class="mb-s">
|
||||||
{{
|
{{
|
||||||
$locale.baseText('workflows.shareModal.info.sharee', {
|
$locale.baseText('workflows.shareModal.info.sharee', {
|
||||||
interpolate: { workflowOwnerName },
|
interpolate: { workflowOwnerName },
|
||||||
|
@ -213,8 +213,9 @@ export default defineComponent({
|
||||||
const isAlreadySharedWithUser = (this.sharedWith || []).find(
|
const isAlreadySharedWithUser = (this.sharedWith || []).find(
|
||||||
(sharee) => sharee.id === user.id,
|
(sharee) => sharee.id === user.id,
|
||||||
);
|
);
|
||||||
|
const isOwner = this.workflow?.ownedBy?.id === user.id;
|
||||||
|
|
||||||
return !isCurrentUser && !isAlreadySharedWithUser;
|
return !isCurrentUser && !isAlreadySharedWithUser && !isOwner;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sharedWithList(): Array<Partial<IUser>> {
|
sharedWithList(): Array<Partial<IUser>> {
|
||||||
|
|
|
@ -64,6 +64,7 @@ export const parsePermissionsTable = (
|
||||||
|
|
||||||
export const getCredentialPermissions = (user: IUser | null, credential: ICredentialsResponse) => {
|
export const getCredentialPermissions = (user: IUser | null, credential: ICredentialsResponse) => {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const rbacStore = useRBACStore();
|
||||||
const isSharingEnabled = settingsStore.isEnterpriseFeatureEnabled(
|
const isSharingEnabled = settingsStore.isEnterpriseFeatureEnabled(
|
||||||
EnterpriseEditionFeature.Sharing,
|
EnterpriseEditionFeature.Sharing,
|
||||||
);
|
);
|
||||||
|
@ -77,10 +78,14 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden
|
||||||
name: UserRole.ResourceSharee,
|
name: UserRole.ResourceSharee,
|
||||||
test: () => !!credential?.sharedWith?.find((sharee) => sharee.id === user?.id),
|
test: () => !!credential?.sharedWith?.find((sharee) => sharee.id === user?.id),
|
||||||
},
|
},
|
||||||
|
{ name: 'read', test: () => rbacStore.hasScope('credential:read') },
|
||||||
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||||
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||||
{ name: 'updateConnection', test: [UserRole.ResourceOwner] },
|
{ name: 'updateConnection', test: [UserRole.ResourceOwner] },
|
||||||
{ name: 'updateSharing', test: [UserRole.ResourceOwner] },
|
{
|
||||||
|
name: 'updateSharing',
|
||||||
|
test: (permissions) => rbacStore.hasScope('credential:share') || !!permissions.isOwner,
|
||||||
|
},
|
||||||
{ name: 'updateNodeAccess', test: [UserRole.ResourceOwner] },
|
{ name: 'updateNodeAccess', test: [UserRole.ResourceOwner] },
|
||||||
{ name: 'delete', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
{ name: 'delete', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||||
{ name: 'use', test: [UserRole.ResourceOwner, UserRole.ResourceSharee] },
|
{ name: 'use', test: [UserRole.ResourceOwner, UserRole.ResourceSharee] },
|
||||||
|
@ -104,7 +109,7 @@ export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'updateSharing',
|
name: 'updateSharing',
|
||||||
test: (permissions) => rbacStore.hasScope('workflow:update') || !!permissions.isOwner,
|
test: (permissions) => rbacStore.hasScope('workflow:share') || !!permissions.isOwner,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
|
|
|
@ -416,7 +416,7 @@
|
||||||
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
||||||
"credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google",
|
"credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google",
|
||||||
"credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.",
|
"credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.",
|
||||||
"credentialEdit.credentialSharing.info.instanceOwner": "You can view this credential because you are the instance owner (and rename or delete it too). To use it in a workflow, ask the credential owner to share it with you.",
|
"credentialEdit.credentialSharing.info.reader": "You can view this credential because you have permission to read and share (and rename or delete it too). To use it in a workflow, ask the credential owner to share it with you.",
|
||||||
"credentialEdit.credentialSharing.info.sharee": "Only {credentialOwnerName} can change who this credential is shared with",
|
"credentialEdit.credentialSharing.info.sharee": "Only {credentialOwnerName} can change who this credential is shared with",
|
||||||
"credentialEdit.credentialSharing.info.sharee.fallback": "the owner",
|
"credentialEdit.credentialSharing.info.sharee.fallback": "the owner",
|
||||||
"credentialEdit.credentialSharing.select.placeholder": "Add users...",
|
"credentialEdit.credentialSharing.select.placeholder": "Add users...",
|
||||||
|
@ -1998,7 +1998,7 @@
|
||||||
"workflows.shareModal.changesHint": "You made changes",
|
"workflows.shareModal.changesHint": "You made changes",
|
||||||
"workflows.shareModal.isDefaultUser.description": "You first need to set up your owner account to enable workflow sharing features.",
|
"workflows.shareModal.isDefaultUser.description": "You first need to set up your owner account to enable workflow sharing features.",
|
||||||
"workflows.shareModal.isDefaultUser.button": "Go to settings",
|
"workflows.shareModal.isDefaultUser.button": "Go to settings",
|
||||||
"workflows.shareModal.info.sharee": "Only {workflowOwnerName} can change who this workflow is shared with",
|
"workflows.shareModal.info.sharee": "Only {workflowOwnerName} or users with workflow sharing permission can change who this workflow is shared with",
|
||||||
"workflows.shareModal.info.sharee.fallback": "the owner",
|
"workflows.shareModal.info.sharee.fallback": "the owner",
|
||||||
"workflows.roles.editor": "Editor",
|
"workflows.roles.editor": "Editor",
|
||||||
"workflows.concurrentChanges.confirmMessage.title": "Workflow was changed by someone else",
|
"workflows.concurrentChanges.confirmMessage.title": "Workflow was changed by someone else",
|
||||||
|
|
|
@ -43,10 +43,9 @@ import { useRBACStore } from '@/stores/rbac.store';
|
||||||
import type { Scope, ScopeLevel } from '@n8n/permissions';
|
import type { Scope, ScopeLevel } from '@n8n/permissions';
|
||||||
import { inviteUsers, acceptInvitation } from '@/api/invitation';
|
import { inviteUsers, acceptInvitation } from '@/api/invitation';
|
||||||
|
|
||||||
const isDefaultUser = (user: IUserResponse | null) =>
|
|
||||||
user?.isPending && user?.globalRole?.name === ROLE.Owner;
|
|
||||||
const isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
|
const isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
|
||||||
const isInstanceOwner = (user: IUserResponse | null) => user?.globalRole?.name === ROLE.Owner;
|
const isInstanceOwner = (user: IUserResponse | null) => user?.globalRole?.name === ROLE.Owner;
|
||||||
|
const isDefaultUser = (user: IUserResponse | null) => isInstanceOwner(user) && isPendingUser(user);
|
||||||
|
|
||||||
export const useUsersStore = defineStore(STORES.USERS, {
|
export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
state: (): IUsersState => ({
|
state: (): IUsersState => ({
|
||||||
|
|
Loading…
Reference in a new issue