@intuitionrobotics/permissions
Version:
226 lines • 10.5 kB
JavaScript
/*
* ts-common is the basic building blocks of our typescript projects
*
* Copyright (C) 2020 Intuition Robotics
*
* 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 { _keys, BadImplementationException, batchActionParallel, filterDuplicates, Module } from "@intuitionrobotics/ts-common";
import { ApiException, ApiResponse } from "@intuitionrobotics/thunderstorm/backend";
import {} from "../../index.js";
import { AccessLevelPermissionsDB, ApiPermissionsDB } from "./db-types/managment.js";
import { GroupPermissionsDB, UserPermissionsDB } from "./db-types/assign.js";
import { HttpMethod } from "@intuitionrobotics/thunderstorm";
import { AccountModule } from "@intuitionrobotics/user-account/backend";
import { PermissionsModule } from "./PermissionsModule.js";
export class PermissionsAssert_Class extends Module {
constructor() {
super("PermissionsAssert");
}
Middleware = (keys) => async (req, data, response, scopes) => {
let account;
await this.CustomMiddleware(keys, async (projectId, customFields) => {
account = await AccountModule.validateSession(req, scopes, response);
await this.assertUserPermissions(projectId, data.url, account._id, customFields);
})(req, data, response, scopes);
return { account };
};
CustomMiddleware = (keys, action) => async (req, data, _response) => {
const customFields = {};
let object;
switch (data.method) {
case HttpMethod.POST:
case HttpMethod.PATCH:
case HttpMethod.PUT:
object = data.body;
break;
case HttpMethod.GET:
case HttpMethod.DELETE:
object = data.query;
break;
default:
throw new BadImplementationException(`Generic custom fields cannot be extracted on api with method: ${data.method}`);
}
_keys(object).filter(key => keys.includes(key)).forEach(key => {
const oElement = object[key];
if (oElement === undefined || oElement === null)
return;
if (typeof oElement !== "string")
return;
customFields[key] = oElement;
});
const projectId = PermissionsModule.getProjectIdentity()._id;
await action(projectId, customFields);
};
async assertUserPermissions(projectId, path, userId, requestCustomField) {
const [apiDetails, userDetails] = await Promise.all([
this.getApiDetails(path, projectId),
this.getUserDetails(userId)
]);
this._assertUserPermissionsImpl(apiDetails, projectId, userDetails, requestCustomField);
}
_assertUserPermissionsImpl(apiDetails, projectId, userDetails, requestCustomField) {
if (!apiDetails.apiDb.accessLevelIds) {
if (!this.config.strictMode)
return;
throw new ApiException(403, `No permissions configuration specified for api: ${projectId}--${apiDetails.apiDb.path}`);
}
this.assertUserPermissionsImpl(userDetails.userGroups, apiDetails.requestPermissions, [requestCustomField]);
}
async assertUserSharingGroup(granterUserId, userGroup) {
const [granterUser, groupToShare] = await Promise.all([this.getUserDetails(granterUserId), GroupPermissionsDB.queryUnique({ _id: userGroup.groupId })]);
groupToShare.customFields = this.getCombineUserGroupCF(userGroup, groupToShare);
const requestPermissions = await this.getAccessLevels(groupToShare.accessLevelIds || []);
const requestCustomFields = groupToShare.customFields;
this.assertUserPermissionsImpl(granterUser.userGroups, requestPermissions, requestCustomFields);
}
assertUserPermissionsImpl(userGroups, requestPermissions, requestCustomFields) {
if (!requestPermissions.length)
return;
const requestPairWithLevelsObj = { accessLevels: requestPermissions, customFields: requestCustomFields };
let groupMatch = false;
const groupsMatchArray = userGroups.map(group => {
const groupPairWithLevelsObj = { accessLevels: group.__accessLevels || [], customFields: group.customFields || [] };
return this.isMatchWithLevelsObj(groupPairWithLevelsObj, requestPairWithLevelsObj);
});
for (const match of groupsMatchArray) {
if (match)
groupMatch = true;
}
if (!groupMatch) {
throw new ApiException(403, "Action Forbidden");
}
}
async getUserDetails(uuid) {
const user = await UserPermissionsDB.queryUnique({ accountId: uuid });
const userGroups = filterDuplicates(user.groups || []);
const groups = await batchActionParallel(userGroups.map(userGroup => userGroup.groupId), 10, subGroupIds => GroupPermissionsDB.query({ where: { _id: { $in: subGroupIds } } }));
return {
user,
userGroups: this.getCombineUserGroups(userGroups, groups)
};
}
getCombineUserGroupCF(userGroup, group) {
const cfArray = [];
if (group.customFields) {
cfArray.push(...group.customFields);
}
if (userGroup.customField) {
cfArray.push(userGroup.customField);
}
return cfArray;
}
getCombineUserGroups(userGroups, groups) {
const combinedGroups = [];
groups.forEach(group => {
const existUserGroupItem = userGroups.find(groupItem => groupItem.groupId === group._id);
if (!existUserGroupItem)
throw new BadImplementationException("You are missing group in your code implementation");
userGroups.forEach((userGroup) => {
if (userGroup.groupId === group._id) {
combinedGroups.push({ ...group, customFields: this.getCombineUserGroupCF(userGroup, group) });
}
});
});
return combinedGroups;
}
async getApiDetails(_path, projectId) {
const path = _path.substring(0, (_path + '?').indexOf('?'));
const apiDb = await ApiPermissionsDB.queryUnique({ path, projectId });
const requestPermissions = await this.getAccessLevels(apiDb.accessLevelIds || []);
return {
apiDb,
requestPermissions
};
}
async getApisDetails(urls, projectId) {
const paths = urls.map(_path => _path.substring(0, (_path + '?').indexOf('?')));
const apiDbs = await batchActionParallel(paths, 10, elements => ApiPermissionsDB.query({ where: { projectId, path: { $in: elements } } }));
return Promise.all(paths.map(async (path) => {
const apiDb = apiDbs.find(_apiDb => _apiDb.path === path);
if (!apiDb)
return;
try {
const requestPermissions = await this.getAccessLevels(apiDb.accessLevelIds);
return ({
apiDb,
requestPermissions
});
}
catch (_e) {
return;
}
}));
}
async getAccessLevels(_accessLevelIds) {
const accessLevelIds = filterDuplicates(_accessLevelIds || []);
const requestPermissions = await batchActionParallel(accessLevelIds, 10, elements => AccessLevelPermissionsDB.query({ where: { _id: { $in: elements } } }));
const idNotFound = accessLevelIds.find(lId => !requestPermissions.find(r => r._id === lId));
if (idNotFound)
throw new ApiException(404, `Could not find api level with _id: ${idNotFound}`);
return requestPermissions;
}
isMatchWithLevelsObj(groupPair, requestPair) {
let match = true;
requestPair.customFields.forEach(requestCF => {
if (!this.doesCustomFieldsSatisfies(groupPair.customFields, requestCF))
match = false;
});
if (!match)
return false;
const groupDomainLevelMap = this.getDomainLevelMap(groupPair.accessLevels);
requestPair.accessLevels.forEach((requiredLevel, _index) => {
const userAccessLevel = groupDomainLevelMap[requiredLevel.domainId];
if (userAccessLevel === undefined || userAccessLevel < requiredLevel.value)
match = false;
});
return match;
}
getDomainLevelMap(accessLevels) {
return accessLevels.reduce((toRet, accessLevel) => {
const levelForDomain = toRet[accessLevel.domainId];
if (!levelForDomain || levelForDomain < accessLevel.value)
toRet[accessLevel.domainId] = accessLevel.value;
return toRet;
}, {});
}
doesCustomFieldsSatisfies(groupCustomFields = [], requestCustomField) {
if (!Object.keys(requestCustomField).length)
return true;
for (const customField of groupCustomFields) {
if (this.doesCustomFieldSatisfies(customField, requestCustomField))
return true;
}
return false;
}
doesCustomFieldSatisfies(groupCustomField, requestCustomField) {
return Object.keys(requestCustomField).reduce((doesSatisfies, requestCustomFieldKey) => {
const customFieldRegEx = this.getRegEx(groupCustomField[requestCustomFieldKey]);
return doesSatisfies && customFieldRegEx.test(requestCustomField[requestCustomFieldKey]);
}, true);
}
getRegEx(value) {
if (!value)
return new RegExp(`^${value}$`, "g");
let regExValue = value;
const startRegEx = '^';
const endRegEx = '$';
if (value[0] !== startRegEx)
regExValue = startRegEx + regExValue;
if (value[value.length - 1] !== endRegEx)
regExValue = regExValue + endRegEx;
return new RegExp(regExValue, "g");
}
}
export const PermissionsAssert = new PermissionsAssert_Class();
//# sourceMappingURL=permissions-assert.js.map