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 = { 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); }