Merge branch 'feature/backend-routing' of github.com:Budibase/budibase into routing-ui
This commit is contained in:
commit
3869ec7c99
|
@ -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) {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue