From 737e9dba47483d7e03845020613eacd9f5c3b7cb Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 22 Apr 2021 11:45:22 +0100 Subject: [PATCH] config specificity --- packages/auth/src/db/utils.js | 15 +-- packages/auth/src/environment.js | 3 - packages/auth/src/index.js | 1 - packages/auth/src/middleware/authenticated.js | 7 +- .../auth/src/middleware/passport/google.js | 22 +--- packages/server/src/api/controllers/user.js | 2 +- packages/server/src/middleware/currentapp.js | 4 +- .../src/api/controllers/admin/configs.js | 116 +++++++++++------- packages/worker/src/api/controllers/auth.js | 13 +- .../worker/src/api/routes/admin/configs.js | 4 +- 10 files changed, 107 insertions(+), 80 deletions(-) diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index ab9142eaee..408daf7dd4 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -98,20 +98,21 @@ exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => { * Generates a new configuration ID. * @returns {string} The new configuration ID which the config doc can be stored under. */ -exports.generateConfigID = (type = "", group = "", user = "") => { - // group += SEPARATOR - const scope = [type, group, user].join(SEPARATOR) +exports.generateConfigID = ({ type, group, user }) => { + const scope = [type, group, user].filter(Boolean).join(SEPARATOR) - return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${newid()}` + return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}` } /** * Gets parameters for retrieving configurations. */ -exports.getConfigParams = (type = "", group = "", otherProps = {}) => { +exports.getConfigParams = ({ type, group, user }, otherProps = {}) => { + const scope = [type, group, user].filter(Boolean).join(SEPARATOR) + return { ...otherProps, - startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${type}${SEPARATOR}${group}`, - endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${type}${SEPARATOR}${group}${UNICODE_MAX}`, + startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`, + endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`, } } diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index e6d7ddda65..3a5c81ea8b 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -2,7 +2,4 @@ module.exports = { JWT_SECRET: process.env.JWT_SECRET, COUCH_DB_URL: process.env.COUCH_DB_URL, SALT_ROUNDS: process.env.SALT_ROUNDS, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, - GOOGLE_AUTH_CALLBACK_URL: process.env.GOOGLE_AUTH_CALLBACK_URL, } diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 1906e20be2..bdc9e16609 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -27,7 +27,6 @@ const { // Strategies passport.use(new LocalStrategy(local.options, local.authenticate)) passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) -// passport.use(new GoogleStrategy(google.options, google.authenticate)) passport.serializeUser((user, done) => done(null, user)) diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index fc3a5b177e..443384ee76 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -1,5 +1,7 @@ const { Cookies } = require("../constants") +const database = require("../db") const { getCookie } = require("../utils") +const { StaticDatabases } = require("../db/utils") module.exports = (noAuthPatterns = []) => { const regex = new RegExp(noAuthPatterns.join("|")) @@ -13,8 +15,11 @@ module.exports = (noAuthPatterns = []) => { const authCookie = getCookie(ctx, Cookies.Auth) if (authCookie) { + const db = database.getDB(StaticDatabases.GLOBAL.name) + const user = await db.get(authCookie.userId) + delete user.password ctx.isAuthenticated = true - ctx.user = authCookie + ctx.user = user } return next() diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index ad7c83d189..ea49b2c35c 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -2,17 +2,7 @@ const env = require("../../environment") const jwt = require("jsonwebtoken") const database = require("../../db") const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy -const { - StaticDatabases, - generateUserID, - generateGlobalUserID, -} = require("../../db/utils") - -exports.options = { - clientID: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, - callbackURL: env.GOOGLE_AUTH_CALLBACK_URL, -} +const { StaticDatabases, generateGlobalUserID } = require("../../db/utils") async function authenticate(token, tokenSecret, profile, done) { // Check the user exists in the instance DB by email @@ -58,16 +48,14 @@ async function authenticate(token, tokenSecret, profile, done) { /** * Create an instance of the google passport strategy. This wrapper fetches the configuration - * from couchDB rather than environment variables, and is necessary for dynamically configuring passport. - * @returns Passport Google Strategy + * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. + * @returns Dynamically configured Passport Google Strategy */ -exports.strategyFactory = async function() { +exports.strategyFactory = async function(scope) { try { const db = database.getDB(StaticDatabases.GLOBAL.name) - const config = await db.get( - "config_google__767bd8f363854dfa8752f593a637b3fd" - ) + const config = await db.get(scope) const { clientID, clientSecret, callbackURL } = config diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 4b6c65736a..1f41acc754 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -72,7 +72,7 @@ exports.createMetadata = async function(ctx) { exports.updateSelfMetadata = async function(ctx) { // overwrite the ID with current users - ctx.request.body._id = ctx.user.userId + ctx.request.body._id = ctx.user._id // make sure no stale rev delete ctx.request.body._rev await exports.updateMetadata(ctx) diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index f429c74267..d85d2158c2 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -31,7 +31,7 @@ module.exports = async (ctx, next) => { appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC) ) { // Different App ID means cookie needs reset, or if the same public user has logged in - const globalId = getGlobalIDFromUserMetadataID(ctx.user.userId) + const globalId = getGlobalIDFromUserMetadataID(ctx.user._id) const globalUser = await getGlobalUsers(ctx, requestAppId, globalId) updateCookie = true appId = requestAppId @@ -50,7 +50,7 @@ module.exports = async (ctx, next) => { ctx.appId = appId if (roleId) { ctx.roleId = roleId - const userId = ctx.user ? generateUserMetadataID(ctx.user.userId) : null + const userId = ctx.user ? generateUserMetadataID(ctx.user._id) : null ctx.user = { ...ctx.user, // override userID with metadata one diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js index c28f0179ee..5e39ebefc0 100644 --- a/packages/worker/src/api/controllers/admin/configs.js +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -1,53 +1,27 @@ const CouchDB = require("../../../db") -const { StaticDatabases, DocumentTypes } = require("@budibase/auth") +const { StaticDatabases } = require("@budibase/auth") const { generateConfigID, getConfigParams } = require("@budibase/auth") -const { SEPARATOR } = require("@budibase/auth/src/db/utils") -const { Configs } = require("../../../constants") const GLOBAL_DB = StaticDatabases.GLOBAL.name -exports.configStatus = async function(ctx) { - const db = new CouchDB(GLOBAL_DB) - let configured = {} - - // check for super admin user - try { - configured.user = true - } catch (err) { - configured.user = false - } - - // check for SMTP config - try { - const response = await db.allDocs( - getConfigParams(`${DocumentTypes.CONFIG}${SEPARATOR}${Configs.SMTP}`) - ) - console.log(response) - configured.smtp = true - } catch (err) { - configured.smtp = false - } - - ctx.body = configured -} - exports.save = async function(ctx) { const db = new CouchDB(GLOBAL_DB) const configDoc = ctx.request.body + const { type, group, user } = configDoc // Config does not exist yet if (!configDoc._id) { - configDoc._id = generateConfigID( - configDoc.type, - configDoc.group, - configDoc.user - ) + configDoc._id = generateConfigID({ + type, + group, + user, + }) } try { const response = await db.post(configDoc) ctx.body = { - type: configDoc.type, + type, _id: response.id, _rev: response.rev, } @@ -67,18 +41,74 @@ exports.fetch = async function(ctx) { ctx.body = groups } +/** + * Gets the most granular config for a particular configuration type. + * The hierarchy is type -> group -> user. + */ exports.find = async function(ctx) { const db = new CouchDB(GLOBAL_DB) - const response = await db.allDocs( - getConfigParams(undefined, { - include_docs: true, - }) - ) - const groups = response.rows.map(row => row.doc) - ctx.body = groups + const userId = ctx.params.user && ctx.params.user._id + + const { group } = ctx.query + if (group) { + const group = await db.get(group) + const userInGroup = group.users.some(groupUser => groupUser === userId) + if (!ctx.user.admin && !userInGroup) { + ctx.throw(400, `User is not in specified group: ${group}.`) + } + } + try { - const record = await db.get(ctx.params.id) - ctx.body = record + const response = await db.allDocs( + getConfigParams( + { + type: ctx.params.type, + user: userId, + group, + }, + { + include_docs: true, + } + ) + ) + const configs = response.rows.map(row => row.doc) + + // Find the config with the most granular scope based on context + const scopedConfig = configs.find(config => { + // Config is specific to a user and a group + if ( + config._id.includes( + generateConfigID({ type: ctx.params.type, user: userId, group }) + ) + ) { + return config + } + + // Config is specific to a user + if ( + config._id.includes( + generateConfigID({ type: ctx.params.type, user: userId }) + ) + ) { + return config + } + + // Config is specific to a group only + if ( + config._id.includes(generateConfigID({ type: ctx.params.type, group })) + ) { + return config + } + + // Config specific to a config type only + return config + }) + + if (scopedConfig) { + ctx.body = scopedConfig + } else { + ctx.throw(400, "No configuration exists.") + } } catch (err) { ctx.throw(err.status, err) } diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index bfc331042e..61025b1a48 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -1,5 +1,6 @@ const authPkg = require("@budibase/auth") const { google } = require("@budibase/auth/src/middleware") +const { Configs } = require("../../constants") const { clearCookie } = authPkg.utils const { Cookies } = authPkg const { passport } = authPkg.auth @@ -35,8 +36,16 @@ exports.logout = async ctx => { ctx.body = { message: "User logged out" } } +/** + * The initial call that google authentication makes to take you to the google login screen. + * On a successful login, you will be redirected to the googleAuth callback route. + */ exports.googlePreAuth = async (ctx, next) => { - const strategy = await google.strategyFactory() + const strategy = await google.strategyFactory({ + type: Configs.GOOGLE, + user: ctx.user._id, + group: ctx.query.group, + }) return passport.authenticate(strategy, { scope: ["profile", "email"], @@ -44,7 +53,7 @@ exports.googlePreAuth = async (ctx, next) => { } exports.googleAuth = async (ctx, next) => { - const strategy = await google.strategyFactory() + const strategy = await google.strategyFactory(ctx) return passport.authenticate( strategy, diff --git a/packages/worker/src/api/routes/admin/configs.js b/packages/worker/src/api/routes/admin/configs.js index 0411a9ffa0..c6ac04619e 100644 --- a/packages/worker/src/api/routes/admin/configs.js +++ b/packages/worker/src/api/routes/admin/configs.js @@ -1,7 +1,6 @@ const Router = require("@koa/router") const controller = require("../../controllers/admin/configs") const joiValidator = require("../../../middleware/joi-validator") -const { authenticated } = require("@budibase/auth") const Joi = require("joi") const { Configs } = require("../../../constants") @@ -16,9 +15,8 @@ function buildConfigSaveValidation() { router .post("/api/admin/configs", buildConfigSaveValidation(), controller.save) - .post("/api/admin/configs/status", controller.configStatus) .delete("/api/admin/configs/:id", controller.destroy) .get("/api/admin/configs", controller.fetch) - .get("/api/admin/configs/:id", controller.find) + .get("/api/admin/configs/:type", controller.find) module.exports = router