fix(editor): Allow owners and admins to share workflows and credentials they don't own (#7833)

This commit is contained in:
Csaba Tuncsik 2023-11-28 11:44:55 +01:00 committed by GitHub
parent 1c6178759c
commit 3ab3ec9da8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 29 additions and 32 deletions

View file

@ -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<

View file

@ -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',

View file

@ -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(

View file

@ -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[] {

View file

@ -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>> {

View file

@ -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',

View file

@ -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",

View file

@ -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 => ({