Merge branch 'feature/backend-routing' of github.com:Budibase/budibase into routing-ui

This commit is contained in:
Martin McKeaveney 2020-11-18 20:27:04 +00:00
commit 3869ec7c99
8 changed files with 186 additions and 79 deletions

View File

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

View File

@ -15,9 +15,11 @@ const {
DocumentTypes,
SEPARATOR,
getPageParams,
getScreenParams,
generatePageID,
generateScreenID,
} = require("../../db/utils")
const { BUILTIN_LEVEL_IDS } = require("../../utilities/security/accessLevels")
const {
downloadExtractComponentLibraries,
} = require("../../utilities/createAppPackage")
@ -27,6 +29,20 @@ const { cloneDeep } = require("lodash/fp")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
// utility function, need to do away with this
async function getMainAndUnauthPage(db) {
let pages = await db.allDocs(
getPageParams(null, {
include_docs: true,
})
)
pages = pages.rows.map(row => row.doc)
const mainPage = pages.find(page => page.name === PageTypes.MAIN)
const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED)
return { mainPage, unauthPage }
}
async function createInstance(template) {
const appId = generateAppID()
@ -67,19 +83,36 @@ exports.fetch = async function(ctx) {
}
}
exports.fetchAppDefinition = async function(ctx) {
const db = new CouchDB(ctx.params.appId)
// TODO: need to get rid of pages here, they shouldn't be needed anymore
const { mainPage, unauthPage } = await getMainAndUnauthPage(db)
const userAccessLevelId =
!ctx.user.accessLevel || !ctx.user.accessLevel._id
? BUILTIN_LEVEL_IDS.PUBLIC
: ctx.user.accessLevel._id
const correctPage =
userAccessLevelId === BUILTIN_LEVEL_IDS.PUBLIC ? unauthPage : mainPage
const screens = (
await db.allDocs(
getScreenParams(correctPage._id, {
include_docs: true,
})
)
).rows.map(row => row.doc)
// TODO: need to handle access control here, limit screens to user access level
ctx.body = {
page: correctPage,
screens: screens,
libraries: ["@budibase/standard-components"],
}
}
exports.fetchAppPackage = async function(ctx) {
const db = new CouchDB(ctx.params.appId)
const application = await db.get(ctx.params.appId)
let pages = await db.allDocs(
getPageParams(null, {
include_docs: true,
})
)
pages = pages.rows.map(row => row.doc)
const mainPage = pages.find(page => page.name === PageTypes.MAIN)
const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED)
const { mainPage, unauthPage } = await getMainAndUnauthPage(db)
ctx.body = {
application,
pages: {

View File

@ -1,6 +1,15 @@
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) {
const screenRoutes = await getRoutingInfo(appId)
const routing = {}
@ -36,13 +45,68 @@ async function getRoutingStructure(appId) {
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 => {
ctx.body = await getRoutingStructure(ctx.appId)
}
exports.clientFetch = async ctx => {
const routing = getRoutingStructure(ctx.appId)
// use the access controller to pick which access level is applicable to this user
const accessController = new AccessController(ctx.appId)
// TODO: iterate through the routes and pick which the user can access
const routing = await getRoutingStructure(ctx.appId)
const accessLevelId = ctx.user.accessLevel._id
// builder is a special case, always return the full routing structure
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

@ -6,6 +6,7 @@ const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router
.get("/api/:appId/definition", controller.fetchAppDefinition)
.get("/api/applications", authorized(BUILDER), controller.fetch)
.get(
"/api/:appId/appPackage",

View File

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

View File

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

View File

@ -1,7 +1,6 @@
const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes")
const accessLevelController = require("../api/controllers/accesslevel")
const { BUILTIN_LEVEL_ID_ARRAY } = require("../utilities/security/accessLevels")
const { getAccessLevel } = require("../utilities/security/accessLevels")
const env = require("../environment")
const { AuthTypes } = require("../constants")
const { getAppId, getCookieName, setCookie } = require("../utilities")
@ -60,31 +59,3 @@ module.exports = async (ctx, 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 { cloneDeep } = require("lodash/fp")
const BUILTIN_IDS = {
ADMIN: "ADMIN",
POWER: "POWER_USER",
BASIC: "BASIC",
ANON: "ANON",
PUBLIC: "PUBLIC",
BUILDER: "BUILDER",
}
function AccessLevel(id, name, inherits = null) {
function AccessLevel(id, name, inherits) {
this._id = id
this.name = name
if (inherits) {
@ -19,8 +20,8 @@ function AccessLevel(id, name, inherits = null) {
exports.BUILTIN_LEVELS = {
ADMIN: new AccessLevel(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER),
POWER: new AccessLevel(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC),
BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.ANON),
ANON: new AccessLevel(BUILTIN_IDS.ANON, "Anonymous"),
BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.PUBLIC),
ANON: new AccessLevel(BUILTIN_IDS.PUBLIC, "Public"),
BUILDER: new AccessLevel(BUILTIN_IDS.BUILDER, "Builder"),
}
@ -36,27 +37,64 @@ function isBuiltin(accessLevel) {
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 {
constructor(appId) {
this.appId = appId
this.accessLevels = {}
}
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
this.userHierarchies = {}
}
async hasAccess(tryingAccessLevelId, userAccessLevelId) {
@ -70,16 +108,16 @@ class AccessController {
) {
return true
}
let userAccess = await this.getAccessLevel(userAccessLevelId)
// check if inherited makes it possible
while (userAccess.inherits) {
if (tryingAccessLevelId === userAccess.inherits) {
return true
}
// go to get the inherited incase it inherits anything
userAccess = await this.getAccessLevel(userAccess.inherits)
let accessLevelIds = this.userHierarchies[userAccessLevelId]
if (!accessLevelIds) {
accessLevelIds = await exports.getUserAccessLevelHierarchy(
this.appId,
userAccessLevelId
)
this.userHierarchies[userAccessLevelId] = userAccessLevelId
}
return false
return accessLevelIds.indexOf(tryingAccessLevelId) !== -1
}
async checkScreensAccess(screens, userAccessLevelId) {