diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index edb4e4c099..0a14440922 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -18,7 +18,7 @@ export type WildcardScope = `${Resource}:*` | '*'; export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share'>; export type TagScope = ResourceScope<'tag'>; export type UserScope = ResourceScope<'user'>; -export type CredentialScope = ResourceScope<'credential'>; +export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share'>; export type VariableScope = ResourceScope<'variable'>; export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>; export type ExternalSecretStoreScope = ResourceScope< diff --git a/packages/cli/src/permissions/roles.ts b/packages/cli/src/permissions/roles.ts index 95cabb03ca..131406aee8 100644 --- a/packages/cli/src/permissions/roles.ts +++ b/packages/cli/src/permissions/roles.ts @@ -17,6 +17,7 @@ export const ownerPermissions: Scope[] = [ 'credential:update', 'credential:delete', 'credential:list', + 'credential:share', 'variable:create', 'variable:read', 'variable:update', diff --git a/packages/editor-ui/src/components/CredentialCard.vue b/packages/editor-ui/src/components/CredentialCard.vue index 80ba4b5498..a6671b82c4 100644 --- a/packages/editor-ui/src/components/CredentialCard.vue +++ b/packages/editor-ui/src/components/CredentialCard.vue @@ -142,7 +142,7 @@ export default defineComponent({ }, async onAction(action: string) { if (action === CREDENTIAL_LIST_ITEM_ACTIONS.OPEN) { - await this.onClick(); + await this.onClick(new Event('click')); } else if (action === CREDENTIAL_LIST_ITEM_ACTIONS.DELETE) { const deleteConfirmed = await this.confirm( this.$locale.baseText( diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue index d0988a16ee..062358cab1 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue @@ -31,28 +31,18 @@ />
- - - + + {{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }} - - {{ $locale.baseText('credentialEdit.credentialSharing.info.instanceOwner') }} + + {{ + $locale.baseText('credentialEdit.credentialSharing.info.sharee', { + interpolate: { credentialOwnerName }, + }) + }} + + + {{ $locale.baseText('credentialEdit.credentialSharing.info.reader') }} sharee.id === user.id, ); + const isOwner = this.credentialData.ownedBy.id === user.id; - return !isCurrentUser && !isAlreadySharedWithUser; + return !isCurrentUser && !isAlreadySharedWithUser && !isOwner; }); }, sharedWithList(): IUser[] { diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index dfb2b4d844..bd9ebc0f08 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -23,7 +23,7 @@
- + {{ $locale.baseText('workflows.shareModal.info.sharee', { interpolate: { workflowOwnerName }, @@ -213,8 +213,9 @@ export default defineComponent({ const isAlreadySharedWithUser = (this.sharedWith || []).find( (sharee) => sharee.id === user.id, ); + const isOwner = this.workflow?.ownedBy?.id === user.id; - return !isCurrentUser && !isAlreadySharedWithUser; + return !isCurrentUser && !isAlreadySharedWithUser && !isOwner; }); }, sharedWithList(): Array> { diff --git a/packages/editor-ui/src/permissions.ts b/packages/editor-ui/src/permissions.ts index 47b1dee9d3..995e9ad6f0 100644 --- a/packages/editor-ui/src/permissions.ts +++ b/packages/editor-ui/src/permissions.ts @@ -64,6 +64,7 @@ export const parsePermissionsTable = ( export const getCredentialPermissions = (user: IUser | null, credential: ICredentialsResponse) => { const settingsStore = useSettingsStore(); + const rbacStore = useRBACStore(); const isSharingEnabled = settingsStore.isEnterpriseFeatureEnabled( EnterpriseEditionFeature.Sharing, ); @@ -77,10 +78,14 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden name: UserRole.ResourceSharee, test: () => !!credential?.sharedWith?.find((sharee) => sharee.id === user?.id), }, + { name: 'read', test: () => rbacStore.hasScope('credential:read') }, { name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] }, { name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] }, { 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: 'delete', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] }, { name: 'use', test: [UserRole.ResourceOwner, UserRole.ResourceSharee] }, @@ -104,7 +109,7 @@ export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb }, { name: 'updateSharing', - test: (permissions) => rbacStore.hasScope('workflow:update') || !!permissions.isOwner, + test: (permissions) => rbacStore.hasScope('workflow:share') || !!permissions.isOwner, }, { name: 'delete', diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index b3dde531fc..4635c040ee 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -416,7 +416,7 @@ "credentialEdit.oAuthButton.connectMyAccount": "Connect my account", "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.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.fallback": "the owner", "credentialEdit.credentialSharing.select.placeholder": "Add users...", @@ -1998,7 +1998,7 @@ "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.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.roles.editor": "Editor", "workflows.concurrentChanges.confirmMessage.title": "Workflow was changed by someone else", diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index 0b2e1e787a..a05c67379a 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -43,10 +43,9 @@ import { useRBACStore } from '@/stores/rbac.store'; import type { Scope, ScopeLevel } from '@n8n/permissions'; 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 isInstanceOwner = (user: IUserResponse | null) => user?.globalRole?.name === ROLE.Owner; +const isDefaultUser = (user: IUserResponse | null) => isInstanceOwner(user) && isPendingUser(user); export const useUsersStore = defineStore(STORES.USERS, { state: (): IUsersState => ({