UNPKG

@intuitionrobotics/permissions

Version:
226 lines • 10.5 kB
/* * 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