Updating to have proper access control via an accessController and nearly ready to spit out the routing structure.

This commit is contained in:
mike12345567 2020-11-16 18:04:44 +00:00
parent 63b08e42aa
commit acdc1e9a56
11 changed files with 132 additions and 117 deletions

View File

@ -8,7 +8,7 @@ const fs = require("fs-extra")
const { join, resolve } = require("../../utilities/centralPath") const { join, resolve } = require("../../utilities/centralPath")
const packageJson = require("../../../package.json") const packageJson = require("../../../package.json")
const { createLinkView } = require("../../db/linkedRows") const { createLinkView } = require("../../db/linkedRows")
const { createRoutingView } = require("../../routing") const { createRoutingView } = require("../../utilities/routing")
const { downloadTemplate } = require("../../utilities/templates") const { downloadTemplate } = require("../../utilities/templates")
const { const {
generateAppID, generateAppID,

View File

@ -1 +1,17 @@
exports.fetch = async ctx => {} const { getRoutingInfo } = require("../../utilities/routing")
const { AccessController } = require("../../utilities/security/accessLevels")
async function getRoutingStructure(appId) {
let baseRouting = await getRoutingInfo(appId)
return baseRouting
}
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)
}

View File

@ -1,20 +1,28 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils") const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/accessLevels")
exports.fetch = async ctx => { exports.fetch = async ctx => {
const db = new CouchDB(ctx.user.appId) const appId = ctx.user.appId
const db = new CouchDB(appId)
const screens = await db.allDocs( const screens = (
await db.allDocs(
getScreenParams(null, { getScreenParams(null, {
include_docs: true, include_docs: true,
}) })
) )
).rows.map(element => element.doc)
ctx.body = screens.rows.map(element => element.doc) ctx.body = await new AccessController(appId).checkScreensAccess(
screens,
ctx.user.accessLevel._id
)
} }
exports.find = async ctx => { exports.find = async ctx => {
const db = new CouchDB(ctx.user.appId) const appId = ctx.user.appId
const db = new CouchDB(appId)
const screens = await db.allDocs( const screens = await db.allDocs(
getScreenParams(ctx.params.pageId, { getScreenParams(ctx.params.pageId, {
@ -22,7 +30,10 @@ exports.find = async ctx => {
}) })
) )
ctx.body = screens.response.rows ctx.body = await new AccessController(appId).checkScreensAccess(
screens,
ctx.user.accessLevel._id
)
} }
exports.save = async ctx => { exports.save = async ctx => {

View File

@ -4,26 +4,7 @@ const compress = require("koa-compress")
const zlib = require("zlib") const zlib = require("zlib")
const { budibaseAppsDir } = require("../utilities/budibaseDir") const { budibaseAppsDir } = require("../utilities/budibaseDir")
const { isDev } = require("../utilities") const { isDev } = require("../utilities")
const { const {mainRoutes, authRoutes, staticRoutes} = require("./routes")
authRoutes,
pageRoutes,
screenRoutes,
userRoutes,
deployRoutes,
applicationRoutes,
rowRoutes,
tableRoutes,
viewRoutes,
staticRoutes,
componentRoutes,
automationRoutes,
accesslevelRoutes,
apiKeysRoutes,
templatesRoutes,
analyticsRoutes,
webhookRoutes,
routingRoutes,
} = require("./routes")
const router = new Router() const router = new Router()
const env = require("../environment") const env = require("../environment")
@ -73,58 +54,15 @@ router.use(authRoutes.routes())
router.use(authRoutes.allowedMethods()) router.use(authRoutes.allowedMethods())
// authenticated routes // authenticated routes
router.use(viewRoutes.routes()) for (let route of mainRoutes) {
router.use(viewRoutes.allowedMethods()) router.use(route.routes())
router.use(route.allowedMethods())
router.use(tableRoutes.routes()) }
router.use(tableRoutes.allowedMethods())
router.use(rowRoutes.routes())
router.use(rowRoutes.allowedMethods())
router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods())
router.use(automationRoutes.routes())
router.use(automationRoutes.allowedMethods())
router.use(webhookRoutes.routes())
router.use(webhookRoutes.allowedMethods())
router.use(deployRoutes.routes())
router.use(deployRoutes.allowedMethods())
router.use(templatesRoutes.routes())
router.use(templatesRoutes.allowedMethods())
// end auth routes
router.use(pageRoutes.routes())
router.use(pageRoutes.allowedMethods())
router.use(screenRoutes.routes())
router.use(screenRoutes.allowedMethods())
router.use(applicationRoutes.routes())
router.use(applicationRoutes.allowedMethods())
router.use(componentRoutes.routes())
router.use(componentRoutes.allowedMethods())
router.use(accesslevelRoutes.routes())
router.use(accesslevelRoutes.allowedMethods())
router.use(apiKeysRoutes.routes())
router.use(apiKeysRoutes.allowedMethods())
router.use(analyticsRoutes.routes())
router.use(analyticsRoutes.allowedMethods())
// WARNING - static routes will catch everything else after them this must be last
router.use(staticRoutes.routes()) router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods()) router.use(staticRoutes.allowedMethods())
router.use(routingRoutes.routes())
router.use(routingRoutes.allowedMethods())
router.redirect("/", "/_builder") router.redirect("/", "/_builder")
module.exports = router module.exports = router

View File

@ -17,9 +17,8 @@ const templatesRoutes = require("./templates")
const analyticsRoutes = require("./analytics") const analyticsRoutes = require("./analytics")
const routingRoutes = require("./routing") const routingRoutes = require("./routing")
module.exports = { exports.mainRoutes = [
deployRoutes, deployRoutes,
authRoutes,
pageRoutes, pageRoutes,
screenRoutes, screenRoutes,
userRoutes, userRoutes,
@ -27,7 +26,6 @@ module.exports = {
rowRoutes, rowRoutes,
tableRoutes, tableRoutes,
viewRoutes, viewRoutes,
staticRoutes,
componentRoutes, componentRoutes,
automationRoutes, automationRoutes,
accesslevelRoutes, accesslevelRoutes,
@ -36,4 +34,7 @@ module.exports = {
analyticsRoutes, analyticsRoutes,
webhookRoutes, webhookRoutes,
routingRoutes, routingRoutes,
} ]
exports.authRoutes = authRoutes
exports.staticRoutes = staticRoutes

View File

@ -5,6 +5,9 @@ const controller = require("../controllers/routing")
const router = Router() const router = Router()
router.post("/api/routing", authorized(BUILDER), controller.fetch) // 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)
module.exports = router module.exports = router

View File

@ -12,10 +12,10 @@ function generateSaveValidation() {
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
_css: Joi.string().allow(""), _css: Joi.string().allow(""),
name: Joi.string().required(), name: Joi.string().required(),
routing: Joi.array().items(Joi.object({ routing: Joi.object({
route: Joi.string().required(), route: Joi.string().required(),
accessLevelId: Joi.string().required(), accessLevelId: Joi.string().required().allow(""),
})).required(), }).required().unknown(true),
props: Joi.object({ props: Joi.object({
_id: Joi.string().required(), _id: Joi.string().required(),
_component: Joi.string().required(), _component: Joi.string().required(),

View File

@ -53,8 +53,6 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
return next() return next()
} }
// TODO: need to handle routing security
if (permType === PermissionTypes.BUILDER) { if (permType === PermissionTypes.BUILDER) {
ctx.throw(403, "Not Authorized") ctx.throw(403, "Not Authorized")
} }

View File

@ -1,11 +1,14 @@
const CouchDB = require("../db") const CouchDB = require("../../db")
const { createRoutingView } = require("./routingUtils") const { createRoutingView } = require("./routingUtils")
const { ViewNames, getQueryIndex } = require("../db/utils") const { ViewNames, getQueryIndex, UNICODE_MAX } = require("../../db/utils")
exports.getRoutingInfo = async appId => { exports.getRoutingInfo = async appId => {
const db = new CouchDB(appId) const db = new CouchDB(appId)
try { try {
const allRouting = await db.query(getQueryIndex(ViewNames.ROUTING)) const allRouting = await db.query(getQueryIndex(ViewNames.ROUTING), {
startKey: "",
endKey: UNICODE_MAX,
})
return allRouting.rows.map(row => row.value) return allRouting.rows.map(row => row.value)
} catch (err) { } catch (err) {
// check if the view doesn't exist, it should for all new instances // check if the view doesn't exist, it should for all new instances

View File

@ -1,19 +1,20 @@
const CouchDB = require("../db") const CouchDB = require("../../db")
const { DocumentTypes, SEPARATOR, ViewNames } = require("../db/utils") const { DocumentTypes, SEPARATOR, ViewNames } = require("../../db/utils")
const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR
exports.createRoutingView = async appId => { exports.createRoutingView = async appId => {
const db = new CouchDB(appId) const db = new CouchDB(appId)
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const view = { const view = {
map: function(doc) { // if using variables in a map function need to inject them before use
if (doc._id.startsWith(SCREEN_PREFIX)) { map: `function(doc) {
if (doc._id.startsWith("${SCREEN_PREFIX}")) {
emit(doc._id, { emit(doc._id, {
id: doc._id, id: doc._id,
routing: doc.routing, routing: doc.routing,
}) })
} }
}.toString(), }`,
} }
designDoc.views = { designDoc.views = {
...designDoc.views, ...designDoc.views,

View File

@ -33,36 +33,80 @@ exports.BUILTIN_LEVEL_NAME_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
) )
function isBuiltin(accessLevel) { function isBuiltin(accessLevel) {
return BUILTIN_IDS.indexOf(accessLevel) !== -1 return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1
} }
exports.getAccessLevel = async (appId, accessLevelId) => { 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)) { if (isBuiltin(accessLevelId)) {
return Object.values(exports.BUILTIN_LEVELS).find( accessLevel = Object.values(exports.BUILTIN_LEVELS).find(
level => level._id === accessLevelId level => level._id === accessLevelId
) )
} else {
const db = new CouchDB(this.appId)
accessLevel = await db.get(accessLevelId)
}
this.accessLevels[accessLevelId] = accessLevel
return accessLevel
} }
const db = new CouchDB(appId)
return await db.get(accessLevelId)
}
exports.hasAccess = async (appId, tryingAccessLevelId, userAccessLevelId) => { async hasAccess(tryingAccessLevelId, userAccessLevelId) {
// special first case, if they are equal then access is allowed, no need to try anything // special cases, the screen has no access level, the access levels are the same or the user
if (tryingAccessLevelId === userAccessLevelId) { // is currently in the builder
if (
tryingAccessLevelId == null ||
tryingAccessLevelId === "" ||
tryingAccessLevelId === userAccessLevelId ||
userAccessLevelId === BUILTIN_IDS.BUILDER
) {
return true return true
} }
let userAccess = await exports.getAccessLevel(appId, userAccessLevelId) let userAccess = await this.getAccessLevel(userAccessLevelId)
// check if inherited makes it possible // check if inherited makes it possible
while (userAccess.inherits) { while (userAccess.inherits) {
if (tryingAccessLevelId === userAccess.inherits) { if (tryingAccessLevelId === userAccess.inherits) {
return true return true
} }
// go to get the inherited incase it inherits anything // go to get the inherited incase it inherits anything
userAccess = await exports.getAccessLevel(appId, userAccess.inherits) userAccess = await this.getAccessLevel(userAccess.inherits)
} }
return false return false
}
async checkScreensAccess(screens, userAccessLevelId) {
let accessibleScreens = []
// don't want to handle this with Promise.all as this would mean all custom access levels would be
// retrieved at same time, it is likely a custom levels will be re-used and therefore want
// to work in sync for performance save
for (let screen of screens) {
const accessible = await this.checkScreenAccess(screen, userAccessLevelId)
if (accessible) {
accessibleScreens.push(accessible)
}
}
return accessibleScreens
}
async checkScreenAccess(screen, userAccessLevelId) {
const accessLevelId =
screen && screen.routing ? screen.routing.accessLevelId : null
if (await this.hasAccess(accessLevelId, userAccessLevelId)) {
return screen
}
return null
}
} }
exports.AccessController = AccessController
exports.BUILTIN_LEVEL_IDS = BUILTIN_IDS exports.BUILTIN_LEVEL_IDS = BUILTIN_IDS
exports.isBuiltin = isBuiltin exports.isBuiltin = isBuiltin
exports.AccessLevel = AccessLevel exports.AccessLevel = AccessLevel