@commercetools-frontend/permissions
Version:
React components to declaratively handle MC permissions
440 lines (404 loc) • 25.7 kB
JavaScript
;
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;