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 a423664f4c
commit 7f5c3a4688
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 packageJson = require("../../../package.json")
const { createLinkView } = require("../../db/linkedRows")
const { createRoutingView } = require("../../routing")
const { createRoutingView } = require("../../utilities/routing")
const { downloadTemplate } = require("../../utilities/templates")
const {
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 { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/accessLevels")
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(
getScreenParams(null, {
include_docs: true,
})
const screens = (
await db.allDocs(
getScreenParams(null, {
include_docs: true,
})
)
).rows.map(element => element.doc)
ctx.body = await new AccessController(appId).checkScreensAccess(
screens,
ctx.user.accessLevel._id
)
ctx.body = screens.rows.map(element => element.doc)
}
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(
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 => {

View File

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

View File

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

View File

@ -5,6 +5,9 @@ const controller = require("../controllers/routing")
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

View File

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

View File

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

View File

@ -1,11 +1,14 @@
const CouchDB = require("../db")
const CouchDB = require("../../db")
const { createRoutingView } = require("./routingUtils")
const { ViewNames, getQueryIndex } = require("../db/utils")
const { ViewNames, getQueryIndex, UNICODE_MAX } = require("../../db/utils")
exports.getRoutingInfo = async appId => {
const db = new CouchDB(appId)
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)
} catch (err) {
// check if the view doesn't exist, it should for all new instances

View File

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

View File

@ -33,36 +33,80 @@ exports.BUILTIN_LEVEL_NAME_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
)
function isBuiltin(accessLevel) {
return BUILTIN_IDS.indexOf(accessLevel) !== -1
return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1
}
exports.getAccessLevel = async (appId, accessLevelId) => {
if (isBuiltin(accessLevelId)) {
return Object.values(exports.BUILTIN_LEVELS).find(
level => level._id === accessLevelId
)
class AccessController {
constructor(appId) {
this.appId = appId
this.accessLevels = {}
}
const db = new CouchDB(appId)
return await db.get(accessLevelId)
}
exports.hasAccess = async (appId, tryingAccessLevelId, userAccessLevelId) => {
// special first case, if they are equal then access is allowed, no need to try anything
if (tryingAccessLevelId === userAccessLevelId) {
return true
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
}
let userAccess = await exports.getAccessLevel(appId, userAccessLevelId)
// check if inherited makes it possible
while (userAccess.inherits) {
if (tryingAccessLevelId === userAccess.inherits) {
async hasAccess(tryingAccessLevelId, userAccessLevelId) {
// special cases, the screen has no access level, the access levels are the same or the user
// is currently in the builder
if (
tryingAccessLevelId == null ||
tryingAccessLevelId === "" ||
tryingAccessLevelId === userAccessLevelId ||
userAccessLevelId === BUILTIN_IDS.BUILDER
) {
return true
}
// go to get the inherited incase it inherits anything
userAccess = await exports.getAccessLevel(appId, userAccess.inherits)
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)
}
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
}
return false
}
exports.AccessController = AccessController
exports.BUILTIN_LEVEL_IDS = BUILTIN_IDS
exports.isBuiltin = isBuiltin
exports.AccessLevel = AccessLevel