UNPKG

@commercetools-frontend/permissions

Version:
440 lines (404 loc) • 25.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('react'); var isNil = require('lodash/isNil'); var invariant = require('tiny-invariant'); var _Object$keys = require('@babel/runtime-corejs3/core-js-stable/object/keys'); var _Object$getOwnPropertySymbols = require('@babel/runtime-corejs3/core-js-stable/object/get-own-property-symbols'); var _filterInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/filter'); var _Object$getOwnPropertyDescriptor = require('@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptor'); var _forEachInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/for-each'); var _Object$getOwnPropertyDescriptors = require('@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptors'); var _Object$defineProperties = require('@babel/runtime-corejs3/core-js-stable/object/define-properties'); var _Object$defineProperty = require('@babel/runtime-corejs3/core-js-stable/object/define-property'); var _defineProperty = require('@babel/runtime-corejs3/helpers/defineProperty'); var _objectWithoutProperties = require('@babel/runtime-corejs3/helpers/objectWithoutProperties'); var warning = require('tiny-warning'); var applicationShellConnectors = require('@commercetools-frontend/application-shell-connectors'); var sentry = require('@commercetools-frontend/sentry'); var _slicedToArray = require('@babel/runtime-corejs3/helpers/slicedToArray'); var _startsWithInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/starts-with'); var _includesInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/includes'); var _everyInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/every'); var _someInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/some'); var _valuesInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/values'); var _findInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/find'); var _Object$entries = require('@babel/runtime-corejs3/core-js-stable/object/entries'); var upperFirst = require('lodash/upperFirst'); var jsxRuntime = require('@emotion/react/jsx-runtime'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } var isNil__default = /*#__PURE__*/_interopDefault(isNil); var invariant__default = /*#__PURE__*/_interopDefault(invariant); var _Object$keys__default = /*#__PURE__*/_interopDefault(_Object$keys); var _Object$getOwnPropertySymbols__default = /*#__PURE__*/_interopDefault(_Object$getOwnPropertySymbols); var _filterInstanceProperty__default = /*#__PURE__*/_interopDefault(_filterInstanceProperty); var _Object$getOwnPropertyDescriptor__default = /*#__PURE__*/_interopDefault(_Object$getOwnPropertyDescriptor); var _forEachInstanceProperty__default = /*#__PURE__*/_interopDefault(_forEachInstanceProperty); var _Object$getOwnPropertyDescriptors__default = /*#__PURE__*/_interopDefault(_Object$getOwnPropertyDescriptors); var _Object$defineProperties__default = /*#__PURE__*/_interopDefault(_Object$defineProperties); var _Object$defineProperty__default = /*#__PURE__*/_interopDefault(_Object$defineProperty); var warning__default = /*#__PURE__*/_interopDefault(warning); var _startsWithInstanceProperty__default = /*#__PURE__*/_interopDefault(_startsWithInstanceProperty); var _includesInstanceProperty__default = /*#__PURE__*/_interopDefault(_includesInstanceProperty); var _everyInstanceProperty__default = /*#__PURE__*/_interopDefault(_everyInstanceProperty); var _someInstanceProperty__default = /*#__PURE__*/_interopDefault(_someInstanceProperty); var _valuesInstanceProperty__default = /*#__PURE__*/_interopDefault(_valuesInstanceProperty); var _findInstanceProperty__default = /*#__PURE__*/_interopDefault(_findInstanceProperty); var _Object$entries__default = /*#__PURE__*/_interopDefault(_Object$entries); var upperFirst__default = /*#__PURE__*/_interopDefault(upperFirst); // 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__default["default"](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__default["default"](permissions).call(permissions, permission => _startsWithInstanceProperty__default["default"](permission).call(permission, 'View')); const impliedPermissions = _filterInstanceProperty__default["default"](viewPermissions).call(viewPermissions, viewPermission => _includesInstanceProperty__default["default"](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__default["default"](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__default["default"](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__default["default"](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__default["default"](options.actualDataFence.dataFenceValue) }); if (!selectedDataFenceData) { sentry.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__default["default"](selectedDataFenceData).call(selectedDataFenceData, value => { var _context; return _includesInstanceProperty__default["default"](_context = _valuesInstanceProperty__default["default"](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__default["default"](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__default["default"](_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__default["default"](_context3 = _Object$entries__default["default"](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) => { react.useEffect(() => { process.env.NODE_ENV !== "production" ? warning__default["default"](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 = applicationShellConnectors.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 = applicationShellConnectors.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 = applicationShellConnectors.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) { sentry.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__default["default"](e); if (_Object$getOwnPropertySymbols__default["default"]) { var o = _Object$getOwnPropertySymbols__default["default"](e); r && (o = _filterInstanceProperty__default["default"](o).call(o, function (r) { return _Object$getOwnPropertyDescriptor__default["default"](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__default["default"](_context = ownKeys$1(Object(t), !0)).call(_context, function (r) { _defineProperty(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors__default["default"] ? _Object$defineProperties__default["default"](e, _Object$getOwnPropertyDescriptors__default["default"](t)) : _forEachInstanceProperty__default["default"](_context2 = ownKeys$1(Object(t))).call(_context2, function (r) { _Object$defineProperty__default["default"](e, r, _Object$getOwnPropertyDescriptor__default["default"](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 jsxRuntime.jsx(jsxRuntime.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 => jsxRuntime.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 jsxRuntime.jsx(Component, _objectSpread$1(_objectSpread$1({}, props), {}, { [propName]: isAuthorized })) }); WrappedComponent.displayName = "withUserPermissions(".concat(getDisplayName(Component), ")"); return WrappedComponent; }; }; const getHasChildren = children => react.Children.count(children) > 0; // Permissions // Action rights // Data fences const RestrictedByPermissions = props => { !!(typeof props.children === 'function' && !isNil__default["default"](props.unauthorizedComponent)) ? process.env.NODE_ENV !== "production" ? invariant__default["default"](false, '@commercetools-frontend/permissions/RestrictedByPermissions: You provided both `children` and `unauthorizedComponent`. Please provide only one of them.') : invariant__default["default"](false) : void 0; return jsxRuntime.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 react.Children.only(props.children); } else if (!isAuthorized) { if (props.unauthorizedComponent) { return /*#__PURE__*/react.createElement(props.unauthorizedComponent); } } return null; } }); }; RestrictedByPermissions.displayName = 'RestrictedByPermissions'; function ownKeys(e, r) { var t = _Object$keys__default["default"](e); if (_Object$getOwnPropertySymbols__default["default"]) { var o = _Object$getOwnPropertySymbols__default["default"](e); r && (o = _filterInstanceProperty__default["default"](o).call(o, function (r) { return _Object$getOwnPropertyDescriptor__default["default"](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__default["default"](_context = ownKeys(Object(t), !0)).call(_context, function (r) { _defineProperty(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors__default["default"] ? _Object$defineProperties__default["default"](e, _Object$getOwnPropertyDescriptors__default["default"](t)) : _forEachInstanceProperty__default["default"](_context2 = ownKeys(Object(t))).call(_context2, function (r) { _Object$defineProperty__default["default"](e, r, _Object$getOwnPropertyDescriptor__default["default"](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 => jsxRuntime.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 jsxRuntime.jsx(Component, _objectSpread({}, props)); } if (FallbackComponent) { return jsxRuntime.jsx(FallbackComponent, {}); } return jsxRuntime.jsx(jsxRuntime.Fragment, {}); } }); WrappedComponent.displayName = "branchOnPermissions(".concat(getDisplayName(Component), ")"); return WrappedComponent; }; }; exports.Authorized = Authorized; exports.RestrictedByPermissions = RestrictedByPermissions; exports.branchOnPermissions = branchOnPermissions; exports.hasEveryPermissions = hasEveryPermissions; exports.hasSomePermissions = hasSomePermissions; exports.injectAuthorized = injectAuthorized; exports.useIsAuthorized = useIsAuthorized; exports.version = version;