client/src/utils/roleModel.js (235 lines of code) (raw):
/*
* Copyright 2017-2019 EPAM Systems, Inc. (https://www.epam.com/)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {inject, observer} from 'mobx-react';
const bitEnabled = (bit, mask) => {
return (mask & bit) === bit;
};
const readAllowed = (item, extendedMask = false) => {
if (!item || item.mask === undefined || item.mask === null) {
return false;
}
if (extendedMask) {
return bitEnabled(1, collapseMask(item.mask));
}
return bitEnabled(1, item.mask);
};
const writeAllowed = (item, extendedMask = false) => {
if (!item || item.mask === undefined || item.mask === null) {
return false;
}
if (extendedMask) {
return bitEnabled(2, collapseMask(item.mask));
}
return bitEnabled(2, item.mask);
};
const executeAllowed = (item, extendedMask = false) => {
if (!item || item.mask === undefined || item.mask === null) {
return false;
}
if (extendedMask) {
return bitEnabled(4, collapseMask(item.mask));
}
return bitEnabled(4, item.mask);
};
const readDenied = (item, extendedMask = false) => {
if (!item || item.mask === undefined || item.mask === null) {
return false;
}
if (extendedMask) {
return bitEnabled(1 << 1, item.mask);
}
return !bitEnabled(1, item.mask);
};
const writeDenied = (item, extendedMask = false) => {
if (!item || item.mask === undefined || item.mask === null) {
return false;
}
if (extendedMask) {
return bitEnabled(1 << 3, item.mask);
}
return !bitEnabled(2, item.mask);
};
const executeDenied = (item, extendedMask = false) => {
if (!item || item.mask === undefined || item.mask === null) {
return false;
}
if (extendedMask) {
return bitEnabled(1 << 5, item.mask);
}
return !bitEnabled(4, item.mask);
};
const isOwner = (item, extendedMask = false) => {
if (!item || item.mask === undefined || item.mask === null) {
return false;
}
if (extendedMask) {
return bitEnabled(8, collapseMask(item.mask));
}
return bitEnabled(8, item.mask);
};
const extendMask = (mask) => {
return (
readAllowed({mask}) |
!readAllowed({mask}) << 1 |
writeAllowed({mask}) << 2 |
!writeAllowed({mask}) << 3 |
executeAllowed({mask}) << 4 |
!executeAllowed({mask}) << 5
);
};
const collapseMask = (mask) => {
let readAllowed = (mask & 1) === 1;
let writeAllowed = (mask & 4) === 4;
let executeAllowed = (mask & 16) === 16;
return readAllowed | writeAllowed << 1 | executeAllowed << 2;
};
const buildMask = (read, write, execute, extendedMask = false) => {
const mask = (read ? 1 : 0) | ((write ? 1 : 0) << 1) | ((execute ? 1 : 0) << 2);
if (extendedMask) {
return extendedMask(mask);
}
return mask;
};
const buildPermissionsMask = (ra, rd, wa, wd, ea, ed) => {
const buildBit = (bit, shift) => bit ? (1 << shift) : 0;
return (
buildBit(ra, 0) |
buildBit(rd, 1) |
buildBit(wa, 2) |
buildBit(wd, 3) |
buildBit(ea, 4) |
buildBit(ed, 5)
);
};
/**
* Checks if specific permission (i.e. read, write or execute) specified by
* `testMinifiedMask` conflict with corresponding permission for the object (`objectMinifiedMask`).
* "Conflict" is a situation when object has a "DENY" rule, but permission (`testMinifiedMask`)
* requests "ALLOW" one.
* @returns {boolean | undefined} `true` if permissions conflict, `false` if dont,
* `undefined` if it depends on inheritance
* @param testMinifiedMask {number} permission mask (2-bit value; first bit describes "allow" rule,
* second bit describes "deny" rule)
* @param objectMinifiedMask {number} object's permission mask (2-bit value; first bit describes
* "allow" rule, second bit describes "deny" rule)
*/
const checkPermissionConflicting = (testMinifiedMask, objectMinifiedMask) => {
if (testMinifiedMask === 0b00) {
return false;
}
if (
testMinifiedMask === 0b11 ||
objectMinifiedMask === 0b00 ||
objectMinifiedMask === 0b11
) {
// for 00 values: permissions are inherited
// for 11 values (impossible situation: both "allow" and "deny" rules): skip
return undefined;
}
return testMinifiedMask === 0b01 && objectMinifiedMask === 0b10;
};
/**
* Checks if permissions specified by `testMask` conflict
* with permissions for the object (`objectMask`). "Conflict" is a situation when
* object has a "DENY" rule, but permissions (`testMask`) request "ALLOW" one.
* @returns {{read: boolean | undefined, write: boolean | undefined, execute: boolean | undefined}}
* For each permission (read, write, execute): `true` if permissions conflict, `false` if dont,
* `undefined` if it depends on inheritance
* @param testMask {number} permissions to check
* @param objectMask {number} object's permissions mask
* @param extendedMask {boolean} true if provided masks are of extended format (i.e. 6-bit format)
*/
const checkPermissionsConflicting = (testMask, objectMask, extendedMask = false) => {
const testMaskExtended = extendedMask ? testMask : extendMask(testMask);
const objectMaskExtended = extendedMask ? objectMask : extendMask(objectMask);
const extractMinifiedPermissionsMask = (mask, shift = 0) => (mask >> shift) & 0b11;
return {
read: checkPermissionConflicting(
extractMinifiedPermissionsMask(testMaskExtended),
extractMinifiedPermissionsMask(objectMaskExtended)
),
write: checkPermissionConflicting(
extractMinifiedPermissionsMask(testMaskExtended, 2),
extractMinifiedPermissionsMask(objectMaskExtended, 2)
),
execute: checkPermissionConflicting(
extractMinifiedPermissionsMask(testMaskExtended, 4),
extractMinifiedPermissionsMask(objectMaskExtended, 4)
)
};
};
/**
* Checks if permissions specified by `testMask` conflict
* with permissions set for the object (`objectMasks`). "Conflict" is a situation when
* object has a "DENY" rule, but permissions (`testMask`) request "ALLOW" one.
* @returns {{read: boolean, write: boolean, execute: boolean}}
* For each permission (read, write, execute): `true` if permissions conflict, `false` if dont
* @param testMask {number} permissions to check
* @param objectMasks {number} object's permissions masks
* @param extendedMask {boolean} true if provided masks are of extended format (i.e. 6-bit format)
*/
/*
Unlike `checkPermissionsConflicting` method, this one does not return `undefined`;
if all permissions specify `undefined` value (i.e. rule is inherited) then there are no "allow"
rules provided and access is denied therefore.
*/
const checkPermissionsSetConflicting = (testMask, objectMasks = [], extendedMask = false) => {
const merged = objectMasks
.map(objectMask => checkPermissionsConflicting(testMask, objectMask, extendedMask))
.reduce((acc, cur) => ({
read: [...(acc.read || []), cur.read],
write: [...(acc.write || []), cur.write],
execute: [...(acc.execute || []), cur.execute]
}), {});
const getResolution = conflictResults => {
const filtered = (conflictResults || []).filter(result => result !== undefined);
if (filtered.length === 0) {
// conflict!
return true;
}
// if we have at least one "true" - there is a conflict
return filtered.find(o => o);
};
return {
read: getResolution(merged.read),
write: getResolution(merged.write),
execute: getResolution(merged.execute)
};
};
/**
* Checks if user or group specified by `sid` has conflicting permissions (`mask`) with ones
* provided for the object (objectPermissions). "Conflict" means that "allow" permission was
* requested but there is at least one "deny" permission for user/group or/and user groups.
* Exceptions: if user is owner or admin, no conflicts will occur.
* @param mask {number} requested access permissions (6-bit format)
* @param sid {{name: string, principal: boolean}} requester
* @param sidRoles {{name: string}[]} user's roles
* @param objectOwner {string} object's owner.
* @param objectPermissions {{mask: number, sid: {name: string, principal: boolean}}[]}
* object permissions
* @returns {{read: boolean, write: boolean, execute: boolean}}
* For each permission (read, write, execute): `true` if permissions conflict (i.e. "allow"
* requested, but object has at least one "deny" rule), `false` if dont
*/
const checkObjectPermissionsConflict = (mask, sid, sidRoles, objectOwner, objectPermissions) => {
const {name, principal} = sid;
const roleNames = (sidRoles || []).map(r => r.name);
if (principal && (name === objectOwner || (roleNames || []).indexOf('ROLE_ADMIN') >= 0)) {
// there are no conflicts if user is owner or admin
return {
read: false,
write: false,
execute: false
};
}
const findMask = (testSid) => (objectPermissions || [])
.find(op => op.sid && op.sid.name === testSid.name && op.sid.principal === testSid.principal)
?.mask;
const findConflicts = (testSid) => {
const objectMask = findMask(testSid);
if (objectMask !== undefined) {
return checkPermissionsConflicting(
mask,
objectMask,
true
);
}
return {};
};
const sids = [sid, ...sidRoles.map(({name}) => ({name, principal: false}))];
const getResolution = conflictResults => {
const filtered = (conflictResults || []).filter(result => result !== undefined);
if (filtered.length === 0) {
// conflict!
return true;
}
// if we have at least one "true" - there is a conflict
return !!filtered.find(o => o);
};
const merged = sids
.map(findConflicts)
.reduce((acc, cur) => ({
read: [...(acc.read || []), cur.read],
write: [...(acc.write || []), cur.write],
execute: [...(acc.execute || []), cur.execute]
}), {});
if (principal) {
// If user has suitable permissions, ignore user's roles permissions
const principalConflicts = findConflicts(sid);
for (const permission of ['read', 'write', 'execute']) {
if (principalConflicts[permission] === false) {
// No conflict (i.e., "false") for "permission"
merged[permission] = [false];
}
}
}
return {
read: getResolution(merged.read),
write: getResolution(merged.write),
execute: getResolution(merged.execute)
};
};
const permissionEnabled = (extendedMask, shift = 0) => {
return (extendedMask >> shift) & 0b11 > 0;
};
const readPermissionEnabled = (extendedMask) => {
return permissionEnabled(extendedMask);
};
const writePermissionEnabled = (extendedMask) => {
return permissionEnabled(extendedMask, 2);
};
const executePermissionEnabled = (extendedMask) => {
return permissionEnabled(extendedMask, 4);
};
const management = (roleName) => (WrappedComponent, key) => {
const Component = inject('authenticatedUserInfo')(
observer(
({authenticatedUserInfo}) => {
if (authenticatedUserInfo.loaded &&
(authenticatedUserInfo.value.admin ||
(authenticatedUserInfo.value.roles || [])
.filter(r => r.name === roleName).length === 1)) {
return WrappedComponent;
}
return null;
}
)
);
return <Component key={key} />;
};
const hasRole = (roleName) => ({props}) => {
const {authenticatedUserInfo} = props;
if (authenticatedUserInfo && authenticatedUserInfo.loaded) {
return authenticatedUserInfo.value.admin ||
(authenticatedUserInfo.value.roles || []).filter(r => r.name === roleName).length === 1;
}
return false;
};
const userHasRole = (user, roleName) => {
if (user && user.roles && user.roles.length > 0 && roleName) {
return user.admin || (user.roles || []).find(r => r.name === roleName);
}
return false;
};
const authenticationInfo = (...opts) => inject('authenticatedUserInfo')(...opts);
const refreshAuthenticationInfo = async ({props}) => {
if (props) {
const {authenticatedUserInfo} = props;
if (authenticatedUserInfo) {
return authenticatedUserInfo.fetch();
}
}
};
function wrapUserIs (role) {
return (user) => userHasRole(user, role);
}
const userIs = {
archiveReader: wrapUserIs('ROLE_STORAGE_ARCHIVE_READER'),
archiveManager: wrapUserIs('ROLE_STORAGE_ARCHIVE_MANAGER'),
storageAdmin: wrapUserIs('ROLE_STORAGE_ADMIN'),
dtsManager: wrapUserIs('ROLE_DTS_MANAGER'),
pipeline: wrapUserIs('ROLE_PIPELINE_MANAGER'),
versionedStorage: wrapUserIs('ROLE_VERSIONED_STORAGE_MANAGER'),
folder: wrapUserIs('ROLE_FOLDER_MANAGER'),
configuration: wrapUserIs('ROLE_CONFIGURATION_MANAGER'),
storage: wrapUserIs('ROLE_STORAGE_MANAGER'),
storageTag: wrapUserIs('ROLE_STORAGE_TAG_MANAGER'),
toolGroup: wrapUserIs('ROLE_TOOL_GROUP_MANAGER'),
entities: wrapUserIs('ROLE_ENTITIES_MANAGER'),
billing: wrapUserIs('ROLE_BILLING_MANAGER')
};
const manager = {
archiveReader: management('ROLE_STORAGE_ARCHIVE_READER'),
archiveManager: management('ROLE_STORAGE_ARCHIVE_MANAGER'),
dtsManager: management('ROLE_DTS_MANAGER'),
storageAdmin: management('ROLE_STORAGE_ADMIN'),
pipeline: management('ROLE_PIPELINE_MANAGER'),
versionedStorage: management('ROLE_VERSIONED_STORAGE_MANAGER'),
folder: management('ROLE_FOLDER_MANAGER'),
configuration: management('ROLE_CONFIGURATION_MANAGER'),
storage: management('ROLE_STORAGE_MANAGER'),
storageTag: management('ROLE_STORAGE_TAG_MANAGER'),
toolGroup: management('ROLE_TOOL_GROUP_MANAGER'),
entities: management('ROLE_ENTITIES_MANAGER'),
billing: management('ROLE_BILLING_MANAGER')
};
const isManager = {
archiveReader: hasRole('ROLE_STORAGE_ARCHIVE_READER'),
archiveManager: hasRole('ROLE_STORAGE_ARCHIVE_MANAGER'),
storageAdmin: hasRole('ROLE_STORAGE_ADMIN'),
dtsManager: hasRole('ROLE_DTS_MANAGER'),
pipeline: hasRole('ROLE_PIPELINE_MANAGER'),
versionedStorage: hasRole('ROLE_VERSIONED_STORAGE_MANAGER'),
folder: hasRole('ROLE_FOLDER_MANAGER'),
configuration: hasRole('ROLE_CONFIGURATION_MANAGER'),
storage: hasRole('ROLE_STORAGE_MANAGER'),
storageTag: hasRole('ROLE_STORAGE_TAG_MANAGER'),
toolGroup: hasRole('ROLE_TOOL_GROUP_MANAGER'),
entities: hasRole('ROLE_ENTITIES_MANAGER'),
billing: hasRole('ROLE_BILLING_MANAGER')
};
export default {
readAllowed,
readDenied,
writeAllowed,
writeDenied,
executeAllowed,
executeDenied,
isOwner,
extendMask,
collapseMask,
manager,
isManager,
userIs,
hasRole,
userHasRole,
authenticationInfo,
refreshAuthenticationInfo,
buildMask,
buildPermissionsMask,
checkPermissionsSetConflicting,
checkObjectPermissionsConflict,
readPermissionEnabled,
writePermissionEnabled,
executePermissionEnabled
};