mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
4.6 KiB
161 lines
4.6 KiB
import { AccessLevel } from '@ghostfolio/common/types';
|
|
import { AccessType } from '@ghostfolio/prisma/enums';
|
|
|
|
/**
|
|
* Scopes describe what a grantee may do on behalf of the granting user. They
|
|
* are a separate axis from the permissions, which describe the capabilities of
|
|
* a role. Both are evaluated, hence a delegation can only narrow the access of
|
|
* the authenticated user and never widen it.
|
|
*/
|
|
export const scopes = {
|
|
accountCreate: 'account:create',
|
|
accountDelete: 'account:delete',
|
|
accountRead: 'account:read',
|
|
accountUpdate: 'account:update',
|
|
activityCreate: 'activity:create',
|
|
activityDelete: 'activity:delete',
|
|
activityRead: 'activity:read',
|
|
activityUpdate: 'activity:update',
|
|
portfolioRead: 'portfolio:read',
|
|
portfolioReadValues: 'portfolio:read:values',
|
|
watchlistCreate: 'watchlist:create',
|
|
watchlistDelete: 'watchlist:delete',
|
|
watchlistRead: 'watchlist:read'
|
|
} as const;
|
|
|
|
export type Scope = (typeof scopes)[keyof typeof scopes];
|
|
|
|
/**
|
|
* Scopes which read data
|
|
*/
|
|
export const SCOPES_OF_READ_ACCESS: readonly Scope[] = [
|
|
scopes.accountRead,
|
|
scopes.activityRead,
|
|
scopes.portfolioRead,
|
|
scopes.portfolioReadValues,
|
|
scopes.watchlistRead
|
|
];
|
|
|
|
/**
|
|
* Scopes which change data
|
|
*/
|
|
export const SCOPES_OF_WRITE_ACCESS: readonly Scope[] = [
|
|
scopes.accountCreate,
|
|
scopes.accountDelete,
|
|
scopes.accountUpdate,
|
|
scopes.activityCreate,
|
|
scopes.activityDelete,
|
|
scopes.activityUpdate,
|
|
scopes.watchlistCreate,
|
|
scopes.watchlistDelete
|
|
];
|
|
|
|
const SCOPES_OF_PUBLIC_ACCESS: readonly Scope[] = [
|
|
scopes.activityRead,
|
|
scopes.portfolioRead
|
|
];
|
|
|
|
export const SCOPES_OF_READ_RESTRICTED_ACCESS: readonly Scope[] =
|
|
SCOPES_OF_READ_ACCESS.filter((scope) => {
|
|
return scope !== scopes.portfolioReadValues;
|
|
});
|
|
|
|
/**
|
|
* Maximum scopes per access type. The scopes stored on an access are
|
|
* intersected with it, hence a scope which the type does not permit stays
|
|
* ineffective even if it is stored. A type which cannot grant the restricted
|
|
* write access drops the write scopes in addition, unless the access reads the
|
|
* monetary values.
|
|
*/
|
|
const SCOPES_OF_TYPE: Record<AccessType, readonly Scope[]> = {
|
|
MCP: [...SCOPES_OF_READ_RESTRICTED_ACCESS, scopes.activityCreate],
|
|
PRIVATE: Object.values(scopes),
|
|
PUBLIC: SCOPES_OF_PUBLIC_ACCESS
|
|
};
|
|
|
|
/**
|
|
* Access types which combine a write scope with the restricted read access,
|
|
* because their tools change data without exposing the monetary values
|
|
*/
|
|
export function canGrantRestrictedWriteAccess({ type }: { type: AccessType }) {
|
|
return type === 'MCP';
|
|
}
|
|
|
|
/**
|
|
* Access level which the scopes of an access grant
|
|
*/
|
|
export function getAccessLevel(aScopes: string[] = []): AccessLevel {
|
|
const hasScopeToReadValues = hasScope(aScopes, scopes.portfolioReadValues);
|
|
|
|
if (hasAnyScopeOfWriteAccess(aScopes)) {
|
|
return hasScopeToReadValues
|
|
? 'CREATE_READ_UPDATE_DELETE'
|
|
: 'CREATE_READ_RESTRICTED_UPDATE_DELETE';
|
|
}
|
|
|
|
return hasScopeToReadValues ? 'READ' : 'READ_RESTRICTED';
|
|
}
|
|
|
|
export function getScopesOfAccess({
|
|
scopes: scopesOfAccess,
|
|
type
|
|
}: {
|
|
scopes?: string[];
|
|
type: AccessType;
|
|
}): string[] {
|
|
const scopesToEvaluate = scopesOfAccess ?? [];
|
|
|
|
const permitsWriteAccess =
|
|
canGrantRestrictedWriteAccess({ type }) ||
|
|
hasScope(scopesToEvaluate, scopes.portfolioReadValues);
|
|
|
|
// An unknown scope is dropped
|
|
return SCOPES_OF_TYPE[type].filter((scope) => {
|
|
return (
|
|
scopesToEvaluate.includes(scope) &&
|
|
(permitsWriteAccess || !SCOPES_OF_WRITE_ACCESS.includes(scope))
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scopes which an access level grants
|
|
*/
|
|
export function getScopesOfAccessLevel(aAccessLevel: AccessLevel): Scope[] {
|
|
switch (aAccessLevel) {
|
|
case 'CREATE_READ_RESTRICTED_UPDATE_DELETE':
|
|
return [...SCOPES_OF_READ_RESTRICTED_ACCESS, ...SCOPES_OF_WRITE_ACCESS];
|
|
case 'CREATE_READ_UPDATE_DELETE':
|
|
return [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS];
|
|
case 'READ':
|
|
return [...SCOPES_OF_READ_ACCESS];
|
|
default:
|
|
return [...SCOPES_OF_READ_RESTRICTED_ACCESS];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scopes of a user acting on their own data, which is unrestricted. The
|
|
* permissions of the role are evaluated separately.
|
|
*/
|
|
export function getScopesOfOwnAccess(): string[] {
|
|
return Object.values(scopes);
|
|
}
|
|
|
|
/**
|
|
* Scopes of an administrator impersonating an arbitrary user, which excludes
|
|
* the monetary values
|
|
*/
|
|
export function getScopesOfUnrestrictedImpersonation(): string[] {
|
|
return [...SCOPES_OF_READ_RESTRICTED_ACCESS];
|
|
}
|
|
|
|
export function hasAnyScopeOfWriteAccess(aScopes: string[] = []) {
|
|
return SCOPES_OF_WRITE_ACCESS.some((scope) => {
|
|
return hasScope(aScopes, scope);
|
|
});
|
|
}
|
|
|
|
export function hasScope(aScopes: string[] = [], aScope: Scope) {
|
|
return aScopes.includes(aScope);
|
|
}
|
|
|