From 6a50b1057d283dd961877698f9a06d14af44ea2c Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 18 Nov 2020 15:12:42 +0000 Subject: [PATCH 1/3] Fixing some bugs with previous commit and updating to add the functionality of the api/routing/client. --- .../src/builderStore/store/frontend.js | 2 +- .../server/src/api/controllers/accesslevel.js | 4 +- .../server/src/api/controllers/routing.js | 74 ++++++++++++- packages/server/src/api/routes/routing.js | 2 +- .../src/api/routes/tests/couchTestUtils.js | 2 +- .../server/src/middleware/authenticated.js | 31 +----- .../src/utilities/security/accessLevels.js | 100 ++++++++++++------ 7 files changed, 144 insertions(+), 71 deletions(-) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index bac3ba84ba..06e9428fa8 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -50,7 +50,7 @@ export const getFrontendStore = () => { return state }) const screens = await api.get("/api/screens").then(r => r.json()) - const routing = await api.get("/api/routing").then(r => r.json()) + const routing = await api.get("/api/routing/client").then(r => r.json()) const mainScreens = screens.filter(screen => screen._id.includes(pkg.pages.main._id) diff --git a/packages/server/src/api/controllers/accesslevel.js b/packages/server/src/api/controllers/accesslevel.js index 22bd1467fb..b2985e2953 100644 --- a/packages/server/src/api/controllers/accesslevel.js +++ b/packages/server/src/api/controllers/accesslevel.js @@ -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) { diff --git a/packages/server/src/api/controllers/routing.js b/packages/server/src/api/controllers/routing.js index bd5d991dfa..763683185a 100644 --- a/packages/server/src/api/controllers/routing.js +++ b/packages/server/src/api/controllers/routing.js @@ -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} 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) } diff --git a/packages/server/src/api/routes/routing.js b/packages/server/src/api/routes/routing.js index 60f84de781..32eb14d390 100644 --- a/packages/server/src/api/routes/routing.js +++ b/packages/server/src/api/routes/routing.js @@ -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 diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 0e137f1a44..85f4c44a62 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -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, } diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index c0fbbdb86c..b30e22f0e1 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -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 -} diff --git a/packages/server/src/utilities/security/accessLevels.js b/packages/server/src/utilities/security/accessLevels.js index 80cc652390..578cd1e803 100644 --- a/packages/server/src/utilities/security/accessLevels.js +++ b/packages/server/src/utilities/security/accessLevels.js @@ -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} 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} 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) { From 82feb6d740162da5764cc7d7722681abc80a4d20 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 18 Nov 2020 15:13:25 +0000 Subject: [PATCH 2/3] Changing back to builder getting the normal routing structure for builder. --- packages/builder/src/builderStore/store/frontend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 06e9428fa8..bac3ba84ba 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -50,7 +50,7 @@ export const getFrontendStore = () => { return state }) const screens = await api.get("/api/screens").then(r => r.json()) - const routing = await api.get("/api/routing/client").then(r => r.json()) + const routing = await api.get("/api/routing").then(r => r.json()) const mainScreens = screens.filter(screen => screen._id.includes(pkg.pages.main._id) From 3cda7ca4899a1e9a96563c3cca607e63c38d46cd Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 18 Nov 2020 18:24:12 +0000 Subject: [PATCH 3/3] Creating a function for the client to be able to pull in client definition from API. --- .../server/src/api/controllers/application.js | 51 +++++++++++++++---- packages/server/src/api/routes/application.js | 1 + 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index dd44bda69a..512299c5c8 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -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: { diff --git a/packages/server/src/api/routes/application.js b/packages/server/src/api/routes/application.js index e3b4ddf6cf..3ee5da058c 100644 --- a/packages/server/src/api/routes/application.js +++ b/packages/server/src/api/routes/application.js @@ -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",