Fixing some bugs with previous commit and updating to add the functionality of the api/routing/client.

This commit is contained in:
mike12345567 2020-11-18 15:12:42 +00:00
parent 63f7641c9e
commit 6a50b1057d
7 changed files with 144 additions and 71 deletions

View File

@ -50,7 +50,7 @@ export const getFrontendStore = () => {
return state return state
}) })
const screens = await api.get("/api/screens").then(r => r.json()) const screens = await api.get("/api/screens").then(r => r.json())
const routing = await api.get("/api/routing").then(r => r.json()) const routing = await api.get("/api/routing/client").then(r => r.json())
const mainScreens = screens.filter(screen => const mainScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.main._id) screen._id.includes(pkg.pages.main._id)

View File

@ -2,6 +2,7 @@ const CouchDB = require("../../db")
const { const {
BUILTIN_LEVELS, BUILTIN_LEVELS,
AccessLevel, AccessLevel,
getAccessLevel,
} = require("../../utilities/security/accessLevels") } = require("../../utilities/security/accessLevels")
const { const {
generateAccessLevelID, generateAccessLevelID,
@ -22,8 +23,7 @@ exports.fetch = async function(ctx) {
} }
exports.find = async function(ctx) { exports.find = async function(ctx) {
const db = new CouchDB(ctx.user.appId) ctx.body = await getAccessLevel(ctx.user.appId, ctx.params.levelId)
ctx.body = await db.get(ctx.params.levelId)
} }
exports.save = async function(ctx) { exports.save = async function(ctx) {

View File

@ -1,6 +1,15 @@
const { getRoutingInfo } = require("../../utilities/routing") const { getRoutingInfo } = require("../../utilities/routing")
const { AccessController } = require("../../utilities/security/accessLevels") const {
getUserAccessLevelHierarchy,
BUILTIN_LEVEL_IDS,
} = require("../../utilities/security/accessLevels")
/**
* Gets the full routing structure by querying the routing view and processing the result into the tree.
* @param {string} appId The application to produce the routing structure for.
* @returns {Promise<object>} The routing structure, this is the full structure designed for use in the builder,
* if the client routing is required then the updateRoutingStructureForUserLevel should be used.
*/
async function getRoutingStructure(appId) { async function getRoutingStructure(appId) {
const screenRoutes = await getRoutingInfo(appId) const screenRoutes = await getRoutingInfo(appId)
const routing = {} const routing = {}
@ -36,13 +45,68 @@ async function getRoutingStructure(appId) {
return { routes: routing } return { routes: routing }
} }
/**
* A function for recursing through the routing structure and adjusting it to match the user's access level
* @param {object} path The routing path, retrieved from the getRoutingStructure function, when this recurses it will
* call with this parameter updated to the various subpaths.
* @param {string[]} accessLevelIds The full list of access level IDs, this has to be passed in as otherwise we would
* need to make this an async function purely for the first call, adds confusion to the recursion.
* @returns {object} The routing structure after it has been updated.
*/
function updateRoutingStructureForUserLevel(path, accessLevelIds) {
for (let routeKey of Object.keys(path)) {
const pathStructure = path[routeKey]
if (pathStructure.subpaths) {
pathStructure.subpaths = updateRoutingStructureForUserLevel(
pathStructure.subpaths,
accessLevelIds
)
}
if (pathStructure.screens) {
const accessLevelOptions = Object.keys(pathStructure.screens)
// starts with highest level and works down through inheritance
let found = false
// special case for when the screen has no access control
if (accessLevelOptions.length === 1 && !accessLevelOptions[0]) {
pathStructure.screenId = pathStructure.screens[accessLevelOptions[0]]
pathStructure.accessLevelId = BUILTIN_LEVEL_IDS.BASIC
found = true
} else {
for (let levelId of accessLevelIds) {
if (accessLevelOptions.indexOf(levelId) !== -1) {
pathStructure.screenId = pathStructure.screens[levelId]
pathStructure.accessLevelId = levelId
found = true
break
}
}
}
// remove the screen options now that we've processed it
delete pathStructure.screens
// if no option was found then remove the route, user can't access it
if (!found) {
delete path[routeKey]
}
}
}
return path
}
exports.fetch = async ctx => { exports.fetch = async ctx => {
ctx.body = await getRoutingStructure(ctx.appId) ctx.body = await getRoutingStructure(ctx.appId)
} }
exports.clientFetch = async ctx => { exports.clientFetch = async ctx => {
const routing = getRoutingStructure(ctx.appId) const routing = await getRoutingStructure(ctx.appId)
// use the access controller to pick which access level is applicable to this user const accessLevelId = ctx.user.accessLevel._id
const accessController = new AccessController(ctx.appId) // builder is a special case, always return the full routing structure
// TODO: iterate through the routes and pick which the user can access if (accessLevelId === BUILTIN_LEVEL_IDS.BUILDER) {
ctx.body = routing
return
}
const accessLevelIds = await getUserAccessLevelHierarchy(
ctx.appId,
accessLevelId
)
ctx.body = updateRoutingStructureForUserLevel(routing.routes, accessLevelIds)
} }

View File

@ -7,7 +7,7 @@ const router = Router()
// gets the full structure, not just the correct screen ID for your access level // gets the full structure, not just the correct screen ID for your access level
router router
.get("/api/routing", authorized(BUILDER), controller.fetch)
.get("/api/routing/client", controller.clientFetch) .get("/api/routing/client", controller.clientFetch)
.get("/api/routing", authorized(BUILDER), controller.fetch)
module.exports = router module.exports = router

View File

@ -188,7 +188,7 @@ const createUserWithPermissions = async (
const anonUser = { const anonUser = {
userId: "ANON", userId: "ANON",
accessLevelId: BUILTIN_LEVEL_IDS.ANON, accessLevelId: BUILTIN_LEVEL_IDS.PUBLIC,
appId: appId, appId: appId,
version: packageJson.version, version: packageJson.version,
} }

View File

@ -1,7 +1,6 @@
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes") const STATUS_CODES = require("../utilities/statusCodes")
const accessLevelController = require("../api/controllers/accesslevel") const { getAccessLevel } = require("../utilities/security/accessLevels")
const { BUILTIN_LEVEL_ID_ARRAY } = require("../utilities/security/accessLevels")
const env = require("../environment") const env = require("../environment")
const { AuthTypes } = require("../constants") const { AuthTypes } = require("../constants")
const { getAppId, getCookieName, setCookie } = require("../utilities") const { getAppId, getCookieName, setCookie } = require("../utilities")
@ -60,31 +59,3 @@ module.exports = async (ctx, next) => {
await next() await next()
} }
/**
* Return the full access level object either from constants
* or the database based on the access level ID passed.
*
* @param {*} appId - appId of the user
* @param {*} accessLevelId - the id of the users access level
*/
const getAccessLevel = async (appId, accessLevelId) => {
if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) {
return {
_id: accessLevelId,
name: accessLevelId,
permissions: [],
}
}
const findAccessContext = {
params: {
levelId: accessLevelId,
},
user: {
appId,
},
}
await accessLevelController.find(findAccessContext)
return findAccessContext.body
}

View File

@ -1,14 +1,15 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { cloneDeep } = require("lodash/fp")
const BUILTIN_IDS = { const BUILTIN_IDS = {
ADMIN: "ADMIN", ADMIN: "ADMIN",
POWER: "POWER_USER", POWER: "POWER_USER",
BASIC: "BASIC", BASIC: "BASIC",
ANON: "ANON", PUBLIC: "PUBLIC",
BUILDER: "BUILDER", BUILDER: "BUILDER",
} }
function AccessLevel(id, name, inherits = null) { function AccessLevel(id, name, inherits) {
this._id = id this._id = id
this.name = name this.name = name
if (inherits) { if (inherits) {
@ -19,8 +20,8 @@ function AccessLevel(id, name, inherits = null) {
exports.BUILTIN_LEVELS = { exports.BUILTIN_LEVELS = {
ADMIN: new AccessLevel(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER), ADMIN: new AccessLevel(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER),
POWER: new AccessLevel(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC), POWER: new AccessLevel(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC),
BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.ANON), BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.PUBLIC),
ANON: new AccessLevel(BUILTIN_IDS.ANON, "Anonymous"), ANON: new AccessLevel(BUILTIN_IDS.PUBLIC, "Public"),
BUILDER: new AccessLevel(BUILTIN_IDS.BUILDER, "Builder"), BUILDER: new AccessLevel(BUILTIN_IDS.BUILDER, "Builder"),
} }
@ -36,27 +37,64 @@ function isBuiltin(accessLevel) {
return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1 return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1
} }
/**
* Gets the access level object, this is mainly useful for two purposes, to check if the level exists and
* to check if the access level inherits any others.
* @param {string} appId The app in which to look for the access level.
* @param {string|null} accessLevelId The level ID to lookup.
* @returns {Promise<AccessLevel|object|null>} The access level object, which may contain an "inherits" property.
*/
exports.getAccessLevel = async (appId, accessLevelId) => {
if (!accessLevelId) {
return null
}
let accessLevel
if (isBuiltin(accessLevelId)) {
accessLevel = cloneDeep(
Object.values(exports.BUILTIN_LEVELS).find(
level => level._id === accessLevelId
)
)
} else {
const db = new CouchDB(appId)
accessLevel = await db.get(accessLevelId)
}
return accessLevel
}
/**
* Returns an ordered array of the user's inherited access level IDs, this can be used
* to determine if a user can access something that requires a specific access level.
* @param {string} appId The ID of the application from which access levels should be obtained.
* @param {string} userAccessLevelId The user's access level, this can be found in their access token.
* @returns {Promise<string[]>} returns an ordered array of the access levels, with the first being their
* highest level of access and the last being the lowest level.
*/
exports.getUserAccessLevelHierarchy = async (appId, userAccessLevelId) => {
// special case, if they don't have a level then they are a public user
if (!userAccessLevelId) {
return [BUILTIN_IDS.PUBLIC]
}
let accessLevelIds = [userAccessLevelId]
let userAccess = await exports.getAccessLevel(appId, userAccessLevelId)
// check if inherited makes it possible
while (
userAccess &&
userAccess.inherits &&
accessLevelIds.indexOf(userAccess.inherits) === -1
) {
accessLevelIds.push(userAccess.inherits)
// go to get the inherited incase it inherits anything
userAccess = await exports.getAccessLevel(appId, userAccess.inherits)
}
// add the user's actual level at the end (not at start as that stops iteration
return accessLevelIds
}
class AccessController { class AccessController {
constructor(appId) { constructor(appId) {
this.appId = appId this.appId = appId
this.accessLevels = {} this.userHierarchies = {}
}
async getAccessLevel(accessLevelId) {
if (this.accessLevels[accessLevelId]) {
return this.accessLevels[accessLevelId]
}
let accessLevel
if (isBuiltin(accessLevelId)) {
accessLevel = Object.values(exports.BUILTIN_LEVELS).find(
level => level._id === accessLevelId
)
} else {
const db = new CouchDB(this.appId)
accessLevel = await db.get(accessLevelId)
}
this.accessLevels[accessLevelId] = accessLevel
return accessLevel
} }
async hasAccess(tryingAccessLevelId, userAccessLevelId) { async hasAccess(tryingAccessLevelId, userAccessLevelId) {
@ -70,16 +108,16 @@ class AccessController {
) { ) {
return true return true
} }
let userAccess = await this.getAccessLevel(userAccessLevelId) let accessLevelIds = this.userHierarchies[userAccessLevelId]
// check if inherited makes it possible if (!accessLevelIds) {
while (userAccess.inherits) { accessLevelIds = await exports.getUserAccessLevelHierarchy(
if (tryingAccessLevelId === userAccess.inherits) { this.appId,
return true userAccessLevelId
} )
// go to get the inherited incase it inherits anything this.userHierarchies[userAccessLevelId] = userAccessLevelId
userAccess = await this.getAccessLevel(userAccess.inherits)
} }
return false
return accessLevelIds.indexOf(tryingAccessLevelId) !== -1
} }
async checkScreensAccess(screens, userAccessLevelId) { async checkScreensAccess(screens, userAccessLevelId) {