From acdc1e9a561050719ae71e78a47d2f3cd9a0da52 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 16 Nov 2020 18:04:44 +0000 Subject: [PATCH] Updating to have proper access control via an accessController and nearly ready to spit out the routing structure. --- .../server/src/api/controllers/application.js | 2 +- .../server/src/api/controllers/routing.js | 18 +++- packages/server/src/api/controllers/screen.js | 29 +++++-- packages/server/src/api/index.js | 74 ++-------------- packages/server/src/api/routes/index.js | 9 +- packages/server/src/api/routes/routing.js | 5 +- packages/server/src/api/routes/screen.js | 6 +- packages/server/src/middleware/authorized.js | 2 - .../src/{ => utilities}/routing/index.js | 9 +- .../{ => utilities}/routing/routingUtils.js | 11 +-- .../src/utilities/security/accessLevels.js | 84 ++++++++++++++----- 11 files changed, 132 insertions(+), 117 deletions(-) rename packages/server/src/{ => utilities}/routing/index.js (76%) rename packages/server/src/{ => utilities}/routing/routingUtils.js (60%) diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 59fb10a5b6..dd44bda69a 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -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, diff --git a/packages/server/src/api/controllers/routing.js b/packages/server/src/api/controllers/routing.js index ec2458a8ca..3be2e3fd41 100644 --- a/packages/server/src/api/controllers/routing.js +++ b/packages/server/src/api/controllers/routing.js @@ -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) +} diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js index 88166bf0b2..694d171fff 100644 --- a/packages/server/src/api/controllers/screen.js +++ b/packages/server/src/api/controllers/screen.js @@ -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 => { diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 0d25e08dee..500ca50ff2 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -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 diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index 2352b2edc2..44f2d08509 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -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 diff --git a/packages/server/src/api/routes/routing.js b/packages/server/src/api/routes/routing.js index f336e4ed67..60f84de781 100644 --- a/packages/server/src/api/routes/routing.js +++ b/packages/server/src/api/routes/routing.js @@ -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 diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index 9bfd7a3973..ce49f66043 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -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(), diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index 5b36448463..5f4b78b97e 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -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") } diff --git a/packages/server/src/routing/index.js b/packages/server/src/utilities/routing/index.js similarity index 76% rename from packages/server/src/routing/index.js rename to packages/server/src/utilities/routing/index.js index 5548f45723..bb0fe5bb62 100644 --- a/packages/server/src/routing/index.js +++ b/packages/server/src/utilities/routing/index.js @@ -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 diff --git a/packages/server/src/routing/routingUtils.js b/packages/server/src/utilities/routing/routingUtils.js similarity index 60% rename from packages/server/src/routing/routingUtils.js rename to packages/server/src/utilities/routing/routingUtils.js index e5a610e70d..5f6a6b5312 100644 --- a/packages/server/src/routing/routingUtils.js +++ b/packages/server/src/utilities/routing/routingUtils.js @@ -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, diff --git a/packages/server/src/utilities/security/accessLevels.js b/packages/server/src/utilities/security/accessLevels.js index a77d9bc6aa..e99b635f39 100644 --- a/packages/server/src/utilities/security/accessLevels.js +++ b/packages/server/src/utilities/security/accessLevels.js @@ -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