n8n/packages/editor-ui/src/stores/credentials.store.ts
Csaba Tuncsik dbd62a4992
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>
2023-12-08 12:52:25 +01:00

440 lines
14 KiB
TypeScript

import type {
INodeUi,
IUsedCredential,
ICredentialMap,
ICredentialsDecryptedResponse,
ICredentialsResponse,
ICredentialsState,
ICredentialTypeMap,
} from '@/Interface';
import {
createNewCredential,
deleteCredential,
getAllCredentials,
getCredentialData,
getCredentialsNewName,
getCredentialTypes,
oAuth1CredentialAuthorize,
oAuth2CredentialAuthorize,
testCredential,
updateCredential,
} from '@/api/credentials';
import { setCredentialSharedWith } from '@/api/credentials.ee';
import { makeRestApiRequest } from '@/utils/apiUtils';
import { getAppNameFromCredType } from '@/utils/nodeTypesUtils';
import { EnterpriseEditionFeature, STORES } from '@/constants';
import { i18n } from '@/plugins/i18n';
import type {
ICredentialsDecrypted,
ICredentialType,
INodeCredentialTestResult,
INodeTypeDescription,
IUser,
} from 'n8n-workflow';
import { defineStore } from 'pinia';
import { useRootStore } from './n8nRoot.store';
import { useNodeTypesStore } from './nodeTypes.store';
import { useSettingsStore } from './settings.store';
import { useUsersStore } from './users.store';
const DEFAULT_CREDENTIAL_NAME = 'Unnamed credential';
const DEFAULT_CREDENTIAL_POSTFIX = 'account';
const TYPES_WITH_DEFAULT_NAME = ['httpBasicAuth', 'oAuth2Api', 'httpDigestAuth', 'oAuth1Api'];
export type CredentialsStore = ReturnType<typeof useCredentialsStore>;
export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
state: (): ICredentialsState => ({
credentialTypes: {},
credentials: {},
}),
getters: {
credentialTypesById(): Record<ICredentialType['name'], ICredentialType> {
return this.credentialTypes;
},
allCredentialTypes(): ICredentialType[] {
return Object.values(this.credentialTypes).sort((a, b) =>
a.displayName.localeCompare(b.displayName),
);
},
allCredentials(): ICredentialsResponse[] {
return Object.values(this.credentials).sort((a, b) => a.name.localeCompare(b.name));
},
allCredentialsByType(): { [type: string]: ICredentialsResponse[] } {
const credentials = this.allCredentials;
const types = this.allCredentialTypes;
return types.reduce(
(accu: { [type: string]: ICredentialsResponse[] }, type: ICredentialType) => {
accu[type.name] = credentials.filter(
(cred: ICredentialsResponse) => cred.type === type.name,
);
return accu;
},
{},
);
},
allUsableCredentialsForNode() {
return (node: INodeUi): ICredentialsResponse[] => {
let credentials: ICredentialsResponse[] = [];
const nodeType = useNodeTypesStore().getNodeType(node.type, node.typeVersion);
if (nodeType?.credentials) {
nodeType.credentials.forEach((cred) => {
credentials = credentials.concat(this.allUsableCredentialsByType[cred.name]);
});
}
return credentials.sort((a, b) => {
const aDate = new Date(a.updatedAt);
const bDate = new Date(b.updatedAt);
return aDate.getTime() - bDate.getTime();
});
};
},
allUsableCredentialsByType(): { [type: string]: ICredentialsResponse[] } {
const credentials = this.allCredentials;
const types = this.allCredentialTypes;
const usersStore = useUsersStore();
return types.reduce(
(accu: { [type: string]: ICredentialsResponse[] }, type: ICredentialType) => {
accu[type.name] = credentials.filter((cred: ICredentialsResponse) => {
return cred.type === type.name && usersStore.isResourceAccessible(cred);
});
return accu;
},
{},
);
},
getCredentialTypeByName() {
return (type: string): ICredentialType | undefined => this.credentialTypes[type];
},
getCredentialById() {
return (id: string): ICredentialsResponse => this.credentials[id];
},
getCredentialByIdAndType() {
return (id: string, type: string): ICredentialsResponse | undefined => {
const credential = this.credentials[id];
return !credential || credential.type !== type ? undefined : credential;
};
},
getCredentialsByType() {
return (credentialType: string): ICredentialsResponse[] => {
return this.allCredentialsByType[credentialType] || [];
};
},
getUsableCredentialByType() {
return (credentialType: string): ICredentialsResponse[] => {
return this.allUsableCredentialsByType[credentialType] || [];
};
},
getNodesWithAccess() {
return (credentialTypeName: string) => {
const nodeTypesStore = useNodeTypesStore();
const allNodeTypes: INodeTypeDescription[] = nodeTypesStore.allNodeTypes;
return allNodeTypes.filter((nodeType: INodeTypeDescription) => {
if (!nodeType.credentials) {
return false;
}
for (const credentialTypeDescription of nodeType.credentials) {
if (credentialTypeDescription.name === credentialTypeName) {
return true;
}
}
return false;
});
};
},
getScopesByCredentialType() {
return (credentialTypeName: string) => {
const credentialType = this.getCredentialTypeByName(credentialTypeName);
if (!credentialType) {
return [];
}
const scopeProperty = credentialType.properties.find((p) => p.name === 'scope');
if (
!scopeProperty ||
!scopeProperty.default ||
typeof scopeProperty.default !== 'string' ||
scopeProperty.default === ''
) {
return [];
}
let { default: scopeDefault } = scopeProperty;
// disregard expressions for display
scopeDefault = scopeDefault.replace(/^=/, '').replace(/\{\{.*\}\}/, '');
if (/ /.test(scopeDefault)) return scopeDefault.split(' ');
if (/,/.test(scopeDefault)) return scopeDefault.split(',');
return [scopeDefault];
};
},
getCredentialOwnerName() {
return (credential: ICredentialsResponse | IUsedCredential | undefined): string => {
return credential?.ownedBy?.firstName
? `${credential.ownedBy.firstName} ${credential.ownedBy.lastName} (${credential.ownedBy.email})`
: i18n.baseText('credentialEdit.credentialSharing.info.sharee.fallback');
};
},
getCredentialOwnerNameById() {
return (credentialId: string): string => {
const credential = this.getCredentialById(credentialId);
return this.getCredentialOwnerName(credential);
};
},
httpOnlyCredentialTypes(): ICredentialType[] {
return this.allCredentialTypes.filter((credentialType) => credentialType.httpRequestNode);
},
},
actions: {
setCredentialTypes(credentialTypes: ICredentialType[]): void {
this.credentialTypes = credentialTypes.reduce(
(accu: ICredentialTypeMap, cred: ICredentialType) => {
accu[cred.name] = cred;
return accu;
},
{},
);
},
setCredentials(credentials: ICredentialsResponse[]): void {
this.credentials = credentials.reduce((accu: ICredentialMap, cred: ICredentialsResponse) => {
if (cred.id) {
accu[cred.id] = cred;
}
return accu;
}, {});
},
addCredentials(credentials: ICredentialsResponse[]): void {
credentials.forEach((cred: ICredentialsResponse) => {
if (cred.id) {
this.credentials[cred.id] = { ...this.credentials[cred.id], ...cred };
}
});
},
upsertCredential(credential: ICredentialsResponse): void {
if (credential.id) {
this.credentials = {
...this.credentials,
[credential.id]: {
...this.credentials[credential.id],
...credential,
},
};
}
},
enableOAuthCredential(credential: ICredentialsResponse): void {
// enable oauth event to track change between modals
},
async fetchCredentialTypes(forceFetch: boolean): Promise<void> {
if (this.allCredentialTypes.length > 0 && !forceFetch) {
return;
}
const rootStore = useRootStore();
const credentialTypes = await getCredentialTypes(rootStore.getBaseUrl);
this.setCredentialTypes(credentialTypes);
},
async fetchAllCredentials(): Promise<ICredentialsResponse[]> {
const rootStore = useRootStore();
const credentials = await getAllCredentials(rootStore.getRestApiContext);
this.setCredentials(credentials);
return credentials;
},
async getCredentialData({
id,
}: {
id: string;
}): Promise<ICredentialsResponse | ICredentialsDecryptedResponse | undefined> {
const rootStore = useRootStore();
return getCredentialData(rootStore.getRestApiContext, id);
},
async createNewCredential(data: ICredentialsDecrypted): Promise<ICredentialsResponse> {
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const credential = await createNewCredential(rootStore.getRestApiContext, data);
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
this.upsertCredential(credential);
if (data.ownedBy) {
this.setCredentialOwnedBy({
credentialId: credential.id,
ownedBy: data.ownedBy,
});
const usersStore = useUsersStore();
if (data.sharedWith && data.ownedBy.id === usersStore.currentUserId) {
await this.setCredentialSharedWith({
credentialId: credential.id,
sharedWith: data.sharedWith,
});
}
}
} else {
this.upsertCredential(credential);
}
return credential;
},
async updateCredential(params: {
data: ICredentialsDecrypted;
id: string;
}): Promise<ICredentialsResponse> {
const { id, data } = params;
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const credential = await updateCredential(rootStore.getRestApiContext, id, data);
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
this.upsertCredential(credential);
if (data.ownedBy) {
this.setCredentialOwnedBy({
credentialId: credential.id,
ownedBy: data.ownedBy,
});
}
} else {
this.upsertCredential(credential);
}
return credential;
},
async deleteCredential({ id }: { id: string }) {
const rootStore = useRootStore();
const deleted = await deleteCredential(rootStore.getRestApiContext, id);
if (deleted) {
const { [id]: deletedCredential, ...rest } = this.credentials;
this.credentials = rest;
}
},
async oAuth2Authorize(data: ICredentialsResponse): Promise<string> {
const rootStore = useRootStore();
return oAuth2CredentialAuthorize(rootStore.getRestApiContext, data);
},
async oAuth1Authorize(data: ICredentialsResponse): Promise<string> {
const rootStore = useRootStore();
return oAuth1CredentialAuthorize(rootStore.getRestApiContext, data);
},
async testCredential(data: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const rootStore = useRootStore();
return testCredential(rootStore.getRestApiContext, { credentials: data });
},
async getNewCredentialName(params: { credentialTypeName: string }): Promise<string> {
try {
const { credentialTypeName } = params;
let newName = DEFAULT_CREDENTIAL_NAME;
if (!TYPES_WITH_DEFAULT_NAME.includes(credentialTypeName)) {
const cred = this.getCredentialTypeByName(credentialTypeName);
newName = cred ? getAppNameFromCredType(cred.displayName) : '';
newName =
newName.length > 0
? `${newName} ${DEFAULT_CREDENTIAL_POSTFIX}`
: DEFAULT_CREDENTIAL_NAME;
}
const rootStore = useRootStore();
const res = await getCredentialsNewName(rootStore.getRestApiContext, newName);
return res.name;
} catch (e) {
return DEFAULT_CREDENTIAL_NAME;
}
},
// Enterprise edition actions
setCredentialOwnedBy(payload: { credentialId: string; ownedBy: Partial<IUser> }) {
this.credentials[payload.credentialId] = {
...this.credentials[payload.credentialId],
ownedBy: payload.ownedBy,
};
},
async setCredentialSharedWith(payload: {
sharedWith: IUser[];
credentialId: string;
}): Promise<ICredentialsResponse> {
if (useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
await setCredentialSharedWith(useRootStore().getRestApiContext, payload.credentialId, {
shareWithIds: payload.sharedWith.map((sharee) => sharee.id),
});
this.credentials[payload.credentialId] = {
...this.credentials[payload.credentialId],
sharedWith: payload.sharedWith,
};
}
return this.credentials[payload.credentialId];
},
addCredentialSharee(payload: { credentialId: string; sharee: Partial<IUser> }): void {
this.credentials[payload.credentialId] = {
...this.credentials[payload.credentialId],
sharedWith: (this.credentials[payload.credentialId].sharedWith || []).concat([
payload.sharee,
]),
};
},
removeCredentialSharee(payload: { credentialId: string; sharee: Partial<IUser> }): void {
this.credentials[payload.credentialId] = {
...this.credentials[payload.credentialId],
sharedWith: (this.credentials[payload.credentialId].sharedWith || []).filter(
(sharee) => sharee.id !== payload.sharee.id,
),
};
},
async getCredentialTranslation(credentialType: string): Promise<object> {
const rootStore = useRootStore();
return makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/credential-translation', {
credentialType,
});
},
},
});
/**
* Helper function for listening to credential changes in the store
*/
export const listenForCredentialChanges = (opts: {
store: CredentialsStore;
onCredentialCreated?: (credential: ICredentialsResponse) => void;
onCredentialUpdated?: (credential: ICredentialsResponse) => void;
onCredentialDeleted?: (credentialId: string) => void;
}): void => {
const { store, onCredentialCreated, onCredentialDeleted, onCredentialUpdated } = opts;
const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential'];
store.$onAction((result) => {
const { name, after, args } = result;
after(async (returnValue) => {
if (!listeningForActions.includes(name)) {
return;
}
switch (name) {
case 'createNewCredential':
const createdCredential = returnValue as ICredentialsResponse;
onCredentialCreated?.(createdCredential);
break;
case 'updateCredential':
const updatedCredential = returnValue as ICredentialsResponse;
onCredentialUpdated?.(updatedCredential);
break;
case 'deleteCredential':
const credentialId = args[0].id;
onCredentialDeleted?.(credentialId);
break;
}
});
});
};