UNPKG

@commercetools-frontend/permissions

Version:
407 lines (375 loc) • 22.5 kB
import { useEffect, Children, createElement } from 'react'; import isNil from 'lodash/isNil'; import invariant from 'tiny-invariant'; import _Object$keys from '@babel/runtime-corejs3/core-js-stable/object/keys'; import _Object$getOwnPropertySymbols from '@babel/runtime-corejs3/core-js-stable/object/get-own-property-symbols'; import _filterInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/filter'; import _Object$getOwnPropertyDescriptor from '@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptor'; import _forEachInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/for-each'; import _Object$getOwnPropertyDescriptors from '@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptors'; import _Object$defineProperties from '@babel/runtime-corejs3/core-js-stable/object/define-properties'; import _Object$defineProperty from '@babel/runtime-corejs3/core-js-stable/object/define-property'; import _defineProperty from '@babel/runtime-corejs3/helpers/esm/defineProperty'; import _objectWithoutProperties from '@babel/runtime-corejs3/helpers/esm/objectWithoutProperties'; import warning from 'tiny-warning'; import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors'; import { reportErrorToSentry } from '@commercetools-frontend/sentry'; import _slicedToArray from '@babel/runtime-corejs3/helpers/esm/slicedToArray'; import _startsWithInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/starts-with'; import _includesInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/includes'; import _everyInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/every'; import _someInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/some'; import _valuesInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/values'; import _findInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/find'; import _Object$entries from '@babel/runtime-corejs3/core-js-stable/object/entries'; import upperFirst from 'lodash/upperFirst'; import { jsx, Fragment } from '@emotion/react/jsx-runtime'; // NOTE: This string will be replaced on build time with the package version. var version = "25.0.0"; // Permissions // Action rights // Data fences // Potentially a union type. // Build the permission key from the definition to match it to the format coming from the API. const toCanCase = permissionName => "can".concat(permissionName); const getIsViewPermission = demandedPermission => _startsWithInstanceProperty(demandedPermission).call(demandedPermission, 'View'); const toManageCase = permissionName => permissionName.replace('View', 'Manage'); // Check that the user permissions match EXACTLY the required permission. // The shapes of the arguments are: // - demandedPermission: // 'ViewProducts' // - actualPermissions: // { canViewProducts: true, canManageOrders: false } const hasExactPermission = (demandedPermission, actualPermissions) => actualPermissions[toCanCase(demandedPermission)]; const getInferredManagePermission = (demandedPermission, actualPermissions) => actualPermissions[toCanCase(toManageCase(demandedPermission))]; // Check that the user permissions match the required MANAGE permission. // The shapes of the arguments are: // - demandedPermission: // 'ViewProducts' // - actualPermissions: // { canViewProducts: true, canManageOrders: false } const doesManagePermissionInferViewPermission = (demandedPermission, actualPermissions) => { const isDemandedPermissionViewPermission = getIsViewPermission(demandedPermission); const isViewPermissionInferredByManagePermission = Boolean(getInferredManagePermission(demandedPermission, actualPermissions)); return isDemandedPermissionViewPermission && isViewPermissionInferredByManagePermission; }; // Check if the demanded permissions contain any unnecessary // implied permissions. // E.g. ViewCustomerGroup is implied by ManageCustomerGroup. const getImpliedPermissions = permissions => { const viewPermissions = _filterInstanceProperty(permissions).call(permissions, permission => _startsWithInstanceProperty(permission).call(permission, 'View')); const impliedPermissions = _filterInstanceProperty(viewPermissions).call(viewPermissions, viewPermission => _includesInstanceProperty(permissions).call(permissions, toManageCase(viewPermission))); return impliedPermissions; }; // Check the user permissions using one of the defined matchers. // The shapes of the arguments are: // - demandedPermission: // 'ViewProducts' // - actualPermissions: // { canViewProducts: true, canManageOrders: false } // NOTE: in case the `actualPermissions` are not defined, fall back to an empty object. // This might be the case when the permissions for a user/project could not be loaded // (e.g. project not found). const hasPermission = (demandedPermission, actualPermissions) => // First checking the existence of the exact permission hasExactPermission(demandedPermission, actualPermissions || {}) || // Then checking if a view permission is inferred as a manage permission doesManagePermissionInferViewPermission(demandedPermission, actualPermissions || {}); // Check the user action rights using one of the defined matchers. // The shapes of the arguments are: // - demandedActionRight: // '{ group: 'products', name: 'editPrices' }' // - actualActionRights: // { orders: { canEditPrices: false }, products: { canEditPrices: true } } const hasActionRight = (demandedActionRight, actualActionRights) => { const actionRightGroup = actualActionRights && actualActionRights[demandedActionRight.group]; return Boolean(actionRightGroup && actionRightGroup[toCanCase(demandedActionRight.name)]); }; // Check that the user permissions match EVERY one of the required permissions. // The shapes of the arguments are: // - demandedPermissions: // ['ViewProducts', 'ManageOrders'] // - actualPermissions: // { canViewProducts: true, canManageOrders: false } const hasEveryPermissions = (demandedPermissions, actualPermissions) => _everyInstanceProperty(demandedPermissions).call(demandedPermissions, permission => hasPermission(permission, actualPermissions)); // Check that the user action rights match EVERY one of the required action rights. // The shapes of the arguments are: // - demandedActionRights: // [ // { group: 'products', name: 'editPrices' }, // { group: 'products', name: 'publishProducts' }, // ] // - actualActionRights: // { products: { canEditPrices: true, canPublishProducts: true } } const hasEveryActionRight = (demandedActionRights, actualActionRights) => _everyInstanceProperty(demandedActionRights).call(demandedActionRights, actionRight => hasActionRight(actionRight, actualActionRights)); // Check that the user permissions match SOME one of the required permissions. // The shapes of the arguments are: // - demandedPermissions: // ['ViewProducts', 'ManageOrders'] // - actualPermissions: // { canViewProducts: true, canManageOrders: false } const hasSomePermissions = (demandedPermissions, actualPermissions) => _someInstanceProperty(demandedPermissions).call(demandedPermissions, permission => hasPermission(permission, actualPermissions)); const getHasDemandedDataFence = options => { if (!options.selectDataFenceData) return false; const hasDemandedPermission = hasPermission(options.demandedDataFence.name, { [options.actualDataFence.name]: true }); if (!hasDemandedPermission) return false; const selectedDataFenceData = options.selectDataFenceData({ type: options.demandedDataFence.type, group: options.demandedDataFence.group, name: options.demandedDataFence.name, actualDataFenceValues: _valuesInstanceProperty(options.actualDataFence.dataFenceValue) }); if (!selectedDataFenceData) { reportErrorToSentry(new Error("missing mapper for type \"".concat(options.demandedDataFence.type, "\"")), { extra: options.demandedDataFence.type }); return false; } // it is enough to only have a subset of demanded dataFence data belonging to actual dataFence values return _someInstanceProperty(selectedDataFenceData).call(selectedDataFenceData, value => { var _context; return _includesInstanceProperty(_context = _valuesInstanceProperty(options.actualDataFence.dataFenceValue)).call(_context, value); }); }; const getDataFenceByTypeAndGroup = (actualDataFences, demandedDataFenceType, demandedDataFenceGroup) => { if (!actualDataFences) return null; if (demandedDataFenceType in actualDataFences) { switch (demandedDataFenceType) { case 'store': { const actualDataFence = actualDataFences[demandedDataFenceType]; if (actualDataFence && demandedDataFenceGroup in actualDataFence) { return actualDataFence[demandedDataFenceGroup]; } break; } } } return null; }; const getIsPermissionOverwritingDataFence = (actualPermissions, demandedDataFence) => { if (!actualPermissions) return false; /** * NOTE: * A data fence relates, exists in relation to a general permission. * This relation is constructed by the group of the data fence. * * Given the user already has manage access on the data fence's group * Then the data fence is overruled by it. * Given the user _does not_ have manage access on the data fence's group * Then the data fence itself takes precedence */ const demandedPermissionByDataFence = "Manage".concat(upperFirst(demandedDataFence.group)); if (hasExactPermission(demandedPermissionByDataFence, actualPermissions)) return true; return false; }; const hasSomeDataFence = options => { var _context2; // Datafences applied should be combined with an OR, that is why we use // `some` and not `every` return _someInstanceProperty(_context2 = options.demandedDataFences).call(_context2, demandedDataFence => { var _context3; // Given that dataFence structure on `applicationContext`, we get the value by a path // e.g given dataFence with { store: { orders: { canManageOrders: { values: } } } } // we read the dataFence by the [type] and [group] // dataFence[type][group] = dataFence.store.group // with value = there is a dataFence to apply, overrules `hasDemandedProjectPermissions` // without value = there is no dataFence to apply, overruled by `hasDemandedProjectPermissions` const actualDataFenceByTypeAndGroup = getDataFenceByTypeAndGroup(options.actualDataFences, demandedDataFence.type, demandedDataFence.group); if (!actualDataFenceByTypeAndGroup) return false; // we try to find if the demanded dataFence is available inside the actual datafences const specificActualDataFence = _findInstanceProperty(_context3 = _Object$entries(actualDataFenceByTypeAndGroup)).call(_context3, _ref => { let _ref2 = _slicedToArray(_ref, 2), dataFenceName = _ref2[0], dataFenceValue = _ref2[1]; // Either the manage permission matches or the // a demanded view permission is implied by a manage permission. const doesDataFenceMatchByName = dataFenceName === toCanCase(demandedDataFence.name) || dataFenceName === toCanCase(toManageCase(demandedDataFence.name)); const doesDataFenceHaveValue = Boolean(dataFenceValue); return doesDataFenceMatchByName && doesDataFenceHaveValue; }); if (!specificActualDataFence) return false; const _specificActualDataFe = _slicedToArray(specificActualDataFence, 2), dataFenceName = _specificActualDataFe[0], dataFenceValue = _specificActualDataFe[1]; const actualDataFence = { name: dataFenceName, // we do the type casting because at this point we are sure that // specificActualDataFence.dataFenceValue is not null dataFenceValue: dataFenceValue }; const isPermissionOverwritingDataFence = getIsPermissionOverwritingDataFence(options.actualPermissions, demandedDataFence); const hasDemandedDataFence = getHasDemandedDataFence({ actualDataFence, demandedDataFence, selectDataFenceData: options.selectDataFenceData }); return !isPermissionOverwritingDataFence && hasDemandedDataFence; }); }; // Permissions // Action rights // Data fences // Log a warning only once, and not on each render. const useWarning = (condition, message) => { useEffect(() => { process.env.NODE_ENV !== "production" ? warning(condition, message) : void 0; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); }; const useIsAuthorized = _ref => { let demandedPermissions = _ref.demandedPermissions, demandedActionRights = _ref.demandedActionRights, demandedDataFences = _ref.demandedDataFences, selectDataFenceData = _ref.selectDataFenceData, _ref$shouldMatchSomeP = _ref.shouldMatchSomePermissions, shouldMatchSomePermissions = _ref$shouldMatchSomeP === void 0 ? false : _ref$shouldMatchSomeP, projectPermissions = _ref.projectPermissions; const impliedPermissions = getImpliedPermissions(demandedPermissions); useWarning(!demandedActionRights || demandedActionRights.length === 1, "@commercetools-frontend/permissions: It is recommended to pass a single demanded action right while using the hook, HoC or component multiple times."); useWarning(!demandedPermissions || demandedPermissions.length === 1, "@commercetools-frontend/permissions: It is recommended to pass a single demanded permission while using the hook, HoC or component multiple times."); useWarning(shouldMatchSomePermissions === false, "@commercetools-frontend/permissions: It is recommended not to use 'shouldMatchSomePermissions' but instead use the hook, HoC or component multiple times."); useWarning(!impliedPermissions || impliedPermissions.length === 0, "@commercetools-frontend/permissions: Demanded permissions contain implied permissions. These are implied: ".concat(impliedPermissions.join(', '), ".")); const actualPermissions = useApplicationContext(applicationContext => { var _projectPermissions$p; return (_projectPermissions$p = projectPermissions === null || projectPermissions === void 0 ? void 0 : projectPermissions.permissions) !== null && _projectPermissions$p !== void 0 ? _projectPermissions$p : applicationContext.permissions; }); const actualActionRights = useApplicationContext(applicationContext => { var _projectPermissions$a; return (_projectPermissions$a = projectPermissions === null || projectPermissions === void 0 ? void 0 : projectPermissions.actionRights) !== null && _projectPermissions$a !== void 0 ? _projectPermissions$a : applicationContext.actionRights; }); const actualDataFences = useApplicationContext(applicationContext => { var _projectPermissions$d; return (_projectPermissions$d = projectPermissions === null || projectPermissions === void 0 ? void 0 : projectPermissions.dataFences) !== null && _projectPermissions$d !== void 0 ? _projectPermissions$d : applicationContext.dataFences; }); // if the user has no permissions and no dataFences assigned to them, they are not authorized if (!actualPermissions && !actualDataFences) return false; let hasDemandedDataFences = false; if (demandedDataFences && demandedDataFences.length > 0) { if (!selectDataFenceData) { reportErrorToSentry(new Error("@commercetools-frontend/permissions/Authorized: Missing data fences selector \"selectDataFenceData\".")); } hasDemandedDataFences = hasSomeDataFence({ actualPermissions, demandedDataFences, actualDataFences, selectDataFenceData }); } const hasDemandedPermissions = shouldMatchSomePermissions ? hasSomePermissions(demandedPermissions, actualPermissions) : hasEveryPermissions(demandedPermissions, actualPermissions); const hasDemandedActionRights = hasEveryActionRight(demandedActionRights || [], actualActionRights); return hasDemandedDataFences || hasDemandedPermissions && hasDemandedActionRights; }; // From https://github.com/acdlite/recompose/blob/master/src/packages/recompose/getDisplayName.js const getDisplayName = Component => { if (typeof Component === 'string') { return Component; } if (!Component) { return undefined; } return Component.displayName || Component.name || 'Component'; }; const _excluded = ["shouldMatchSomePermissions"]; function ownKeys$1(e, r) { var t = _Object$keys(e); if (_Object$getOwnPropertySymbols) { var o = _Object$getOwnPropertySymbols(e); r && (o = _filterInstanceProperty(o).call(o, function (r) { return _Object$getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread$1(e) { for (var r = 1; r < arguments.length; r++) { var _context, _context2; var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? _forEachInstanceProperty(_context = ownKeys$1(Object(t), !0)).call(_context, function (r) { _defineProperty(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors ? _Object$defineProperties(e, _Object$getOwnPropertyDescriptors(t)) : _forEachInstanceProperty(_context2 = ownKeys$1(Object(t))).call(_context2, function (r) { _Object$defineProperty(e, r, _Object$getOwnPropertyDescriptor(t, r)); }); } return e; } const Authorized = _ref => { let _ref$shouldMatchSomeP = _ref.shouldMatchSomePermissions, shouldMatchSomePermissions = _ref$shouldMatchSomeP === void 0 ? false : _ref$shouldMatchSomeP, props = _objectWithoutProperties(_ref, _excluded); const isAuthorized = useIsAuthorized({ demandedPermissions: props.demandedPermissions, demandedActionRights: props.demandedActionRights, demandedDataFences: props.demandedDataFences, selectDataFenceData: props.selectDataFenceData, shouldMatchSomePermissions: shouldMatchSomePermissions, projectPermissions: props.projectPermissions }); return jsx(Fragment, { children: props.render(isAuthorized) }); }; Authorized.displayName = 'Authorized'; const injectAuthorized = function (demandedPermissions) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let propName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'isAuthorized'; return Component => { const WrappedComponent = props => jsx(Authorized, { shouldMatchSomePermissions: options.shouldMatchSomePermissions, demandedPermissions: demandedPermissions, demandedActionRights: options.actionRights, demandedDataFences: options.dataFences, selectDataFenceData: options.getSelectDataFenceData && options.getSelectDataFenceData(props), render: isAuthorized => // @ts-ignore: relates to https://github.com/emotion-js/emotion/issues/3245 jsx(Component, _objectSpread$1(_objectSpread$1({}, props), {}, { [propName]: isAuthorized })) }); WrappedComponent.displayName = "withUserPermissions(".concat(getDisplayName(Component), ")"); return WrappedComponent; }; }; const getHasChildren = children => Children.count(children) > 0; // Permissions // Action rights // Data fences const RestrictedByPermissions = props => { !!(typeof props.children === 'function' && !isNil(props.unauthorizedComponent)) ? process.env.NODE_ENV !== "production" ? invariant(false, '@commercetools-frontend/permissions/RestrictedByPermissions: You provided both `children` and `unauthorizedComponent`. Please provide only one of them.') : invariant(false) : void 0; return jsx(Authorized, { shouldMatchSomePermissions: props.shouldMatchSomePermissions, demandedPermissions: props.permissions, demandedActionRights: props.actionRights, demandedDataFences: props.dataFences, selectDataFenceData: props.selectDataFenceData, projectPermissions: props.projectPermissions, render: isAuthorized => { if (typeof props.children === 'function') return props.children({ isAuthorized }); if (typeof props.render === 'function') return props.render({ isAuthorized }); if (isAuthorized) { if (props.children && getHasChildren(props.children)) return Children.only(props.children); } else if (!isAuthorized) { if (props.unauthorizedComponent) { return /*#__PURE__*/createElement(props.unauthorizedComponent); } } return null; } }); }; RestrictedByPermissions.displayName = 'RestrictedByPermissions'; function ownKeys(e, r) { var t = _Object$keys(e); if (_Object$getOwnPropertySymbols) { var o = _Object$getOwnPropertySymbols(e); r && (o = _filterInstanceProperty(o).call(o, function (r) { return _Object$getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var _context, _context2; var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? _forEachInstanceProperty(_context = ownKeys(Object(t), !0)).call(_context, function (r) { _defineProperty(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors ? _Object$defineProperties(e, _Object$getOwnPropertyDescriptors(t)) : _forEachInstanceProperty(_context2 = ownKeys(Object(t))).call(_context2, function (r) { _Object$defineProperty(e, r, _Object$getOwnPropertyDescriptor(t, r)); }); } return e; } const branchOnPermissions = function (demandedPermissions, FallbackComponent) { let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { shouldMatchSomePermissions: false }; return Component => { const WrappedComponent = props => jsx(Authorized, { shouldMatchSomePermissions: options.shouldMatchSomePermissions, demandedPermissions: demandedPermissions, demandedActionRights: options.actionRights, demandedDataFences: options.dataFences, selectDataFenceData: options.getSelectDataFenceData && options.getSelectDataFenceData(props), render: isAuthorized => { if (isAuthorized) { // @ts-ignore: relates to https://github.com/emotion-js/emotion/issues/3245 return jsx(Component, _objectSpread({}, props)); } if (FallbackComponent) { return jsx(FallbackComponent, {}); } return jsx(Fragment, {}); } }); WrappedComponent.displayName = "branchOnPermissions(".concat(getDisplayName(Component), ")"); return WrappedComponent; }; }; export { Authorized, RestrictedByPermissions, branchOnPermissions, hasEveryPermissions, hasSomePermissions, injectAuthorized, useIsAuthorized, version };