/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ import { ApiTransformedOperationName } from '@ada/api/client/types'; import { ENV_STORYBOOK, ENV_TEST } from '$config'; import { isDataEqual } from '$common/utils'; import { useHistory } from 'react-router-dom'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; export type ACCESS = 'ALLOW' | 'DENY'; export interface OperationAccess { access: ACCESS; isAllowed: boolean; isDenied: boolean; } export type ApiPermissions = Partial<{ [P in ApiTransformedOperationName]: boolean; }>; export function getOperationAccess(allow: boolean): OperationAccess { // TODO: move this to auto mocking once adding tests - current here to prevent all other tests from failing if (ENV_TEST) { return { access: 'ALLOW', isAllowed: true, isDenied: false, }; } return { access: allow ? 'ALLOW' : 'DENY', isAllowed: allow === true, isDenied: allow !== true, }; } interface PermissionsContext { defaultAccess: ACCESS; permissions: ApiPermissions; applyPermissions: (permissions: ApiPermissions) => ApiPermissions; isAllowed: (operation: ApiTransformedOperationName) => boolean; } const PermissionsContext = createContext<PermissionsContext | undefined>(undefined); //NOSONAR (S2814:Duplicate) - false positive - type vs value export const usePermissionsContext = (): PermissionsContext => { const context = useContext(PermissionsContext); if (context == null) { if (ENV_TEST || ENV_STORYBOOK) { return TEST_CONTEXT; } throw new Error('Must wrap in PermissionsContext.Provider'); } return context; }; export const useOperationAccess = (operation: ApiTransformedOperationName): OperationAccess => { const { isAllowed } = usePermissionsContext(); return useMemo<OperationAccess>(() => { return getOperationAccess(isAllowed(operation)); }, [isAllowed, operation]); }; /** * Indicates if every operation in list is allowed for the current user. * @param operations List of operation names to check * @returns {boolean} Returns `true` is every operation is allow, otherwise `false` */ export const useOperationAllowed = <T extends ApiTransformedOperationName[]>(...operations: T): boolean => { const { isAllowed } = usePermissionsContext(); return operations.every((operation) => isAllowed(operation)); }; /** * Redirect user when operation(s) are not allowed. * @param redirect Path to redirect to * @param operations List of operations to verify are allowed * @returns */ export const useOperationDeniedRedirect = <T extends ApiTransformedOperationName[]>( redirect: string, ...operations: T ): boolean => { const history = useHistory(); const allowed = useOperationAllowed(...operations); useEffect(() => { if (allowed !== true) { history.push(redirect); } }, [allowed, redirect]); return allowed; }; const DEFAULT_ACCESS: ACCESS = ENV_TEST || ENV_STORYBOOK ? 'ALLOW' : 'DENY'; export const PermissionsProvider: React.FC< Partial<Pick<PermissionsContext, 'defaultAccess'>> & { basePermissions?: ApiPermissions } > = ({ children, defaultAccess = DEFAULT_ACCESS, basePermissions }) => { const [permissions, setPermissions] = useState<PermissionsContext['permissions']>(basePermissions || {}); const applyPermissions = useCallback<PermissionsContext['applyPermissions']>( (newPermissions) => { const _permissions = { ...newPermissions, ...basePermissions, }; // only update permissions change if has changed if (!isDataEqual(permissions, _permissions)) { setPermissions(_permissions); } return _permissions; }, [basePermissions, permissions], ); const isAllowed = useCallback<PermissionsContext['isAllowed']>( (operation) => { if (operation in permissions) { return permissions[operation] === true; } return defaultAccess === 'ALLOW'; }, [permissions, defaultAccess], ); const context = useMemo<PermissionsContext>(() => { if (ENV_TEST) return TEST_CONTEXT; return { defaultAccess, permissions, applyPermissions, isAllowed, }; }, [defaultAccess, permissions, applyPermissions, isAllowed]); return <PermissionsContext.Provider value={context}>{children}</PermissionsContext.Provider>; }; const TEST_CONTEXT: PermissionsContext = { defaultAccess: 'ALLOW', permissions: {}, applyPermissions: (p) => p, isAllowed: () => true, };