From 301f681c882567219a1b115aed2a23a74a947dfb Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 20 Apr 2021 18:14:36 +0100 Subject: [PATCH 01/10] config creation and management APIs --- packages/auth/src/db/utils.js | 26 +++++++- packages/auth/src/index.js | 4 ++ .../auth/src/middleware/passport/google.js | 14 +++-- .../src/api/controllers/admin/configs.js | 60 +++++++++++++++++++ .../worker/src/api/routes/admin/configs.js | 28 +++++++++ packages/worker/src/api/routes/index.js | 3 +- packages/worker/src/constants/index.js | 6 ++ 7 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 packages/worker/src/api/controllers/admin/configs.js create mode 100644 packages/worker/src/api/routes/admin/configs.js diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 89aedb60bf..32034bd731 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -10,6 +10,7 @@ const DocumentTypes = { USER: "us", APP: "app", GROUP: "group", + CONFIG: "config", } exports.DocumentTypes = DocumentTypes @@ -52,7 +53,7 @@ exports.getGroupParams = (id = "", otherProps = {}) => { } /** - * Gets parameters for retrieving users. + * Gets parameters for retrieving users, this is a utility function for the getDocParams function. */ exports.getUserParams = (email = "", otherProps = {}) => { if (!email) { @@ -64,3 +65,26 @@ exports.getUserParams = (email = "", otherProps = {}) => { endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`, } } + +/** + * Generates a new configuration ID. + * @returns {string} The new configuration ID which the config doc can be stored under. + */ +exports.generateConfigID = (type = "", group = "") => { + group += SEPARATOR + + return `${ + DocumentTypes.CONFIG + }${SEPARATOR}${type}${SEPARATOR}${group}${newid()}` +} + +/** + * Gets parameters for retrieving configurations. + */ +exports.getConfigParams = (type = "", group = "", otherProps = {}) => { + return { + ...otherProps, + startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${type}${SEPARATOR}${group}`, + endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${type}${SEPARATOR}${group}${UNICODE_MAX}`, + } +} diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 455181d538..4c6ece1b62 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -20,6 +20,8 @@ const { generateGroupID, getGroupParams, getEmailFromUserID, + generateConfigID, + getConfigParams, } = require("./db/utils") // Strategies @@ -54,6 +56,8 @@ module.exports = { generateGroupID, getGroupParams, getEmailFromUserID, + generateConfigID, + getConfigParams, hash, compare, getAppId, diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index 05b435aedd..6246e5e768 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -6,7 +6,13 @@ exports.options = { callbackURL: env.GOOGLE_AUTH_CALLBACK_URL, } -// exports.authenticate = async function(token, tokenSecret, profile, done) { -// // retrieve user ... -// fetchUser().then(user => done(null, user)) -// } +exports.authenticate = async function(token, tokenSecret, profile, done) { + console.log({ + token, + tokenSecret, + profile, + done, + }) + // retrieve user ... + // fetchUser().then(user => done(null, user)) +} diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js new file mode 100644 index 0000000000..e9fc8a3942 --- /dev/null +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -0,0 +1,60 @@ +const CouchDB = require("../../../db") +const { StaticDatabases } = require("@budibase/auth") +const { generateConfigID } = require("@budibase/auth") +const { getConfigParams } = require("@budibase/auth/src/db/utils") + +const GLOBAL_DB = StaticDatabases.GLOBAL.name + +exports.save = async function(ctx) { + const db = new CouchDB(GLOBAL_DB) + const configDoc = ctx.request.body + + // Config does not exist yet + if (!configDoc._id) { + configDoc._id = generateConfigID(configDoc.type, configDoc.group) + } + + try { + const response = await db.post(configDoc) + ctx.body = { + type: configDoc.type, + _id: response.id, + _rev: response.rev, + } + } catch (err) { + ctx.throw(err.status, err) + } +} + +exports.fetch = 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 +} + +exports.find = async function(ctx) { + const db = new CouchDB(GLOBAL_DB) + try { + const record = await db.get(ctx.params.id) + ctx.body = record + } catch (err) { + ctx.throw(err.status, err) + } +} + +exports.destroy = async function(ctx) { + const db = new CouchDB(GLOBAL_DB) + const { id, rev } = ctx.params + + try { + await db.remove(id, rev) + ctx.body = { message: "Config deleted successfully" } + } catch (err) { + ctx.throw(err.status, err) + } +} diff --git a/packages/worker/src/api/routes/admin/configs.js b/packages/worker/src/api/routes/admin/configs.js new file mode 100644 index 0000000000..5b7354bfc3 --- /dev/null +++ b/packages/worker/src/api/routes/admin/configs.js @@ -0,0 +1,28 @@ +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") + +const router = Router() + +function buildConfigSaveValidation() { + // prettier-ignore + return joiValidator.body(Joi.object({ + type: Joi.string().valid(...Object.values(Configs)).required() + }).required().unknown(true)) +} + +router + .post( + "/api/admin/configs", + buildConfigSaveValidation(), + authenticated, + controller.save + ) + .delete("/api/admin/configs/:id", authenticated, controller.destroy) + .get("/api/admin/configs", authenticated, controller.fetch) + .get("/api/admin/configs/:id", authenticated, controller.find) + +module.exports = router diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index 5c6b088443..aa1c6874e3 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -1,6 +1,7 @@ const userRoutes = require("./admin/users") +const configRoutes = require("./admin/configs") const groupRoutes = require("./admin/groups") const authRoutes = require("./auth") const appRoutes = require("./app") -exports.routes = [userRoutes, groupRoutes, authRoutes, appRoutes] +exports.routes = [configRoutes, userRoutes, groupRoutes, authRoutes, appRoutes] diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js index 586d69c86f..76245a3d7d 100644 --- a/packages/worker/src/constants/index.js +++ b/packages/worker/src/constants/index.js @@ -6,3 +6,9 @@ exports.UserStatus = { exports.Groups = { ALL_USERS: "all_users", } + +exports.Configs = { + SETTINGS: "settings", + ACCOUNT: "account", + SMTP: "smtp", +} From ffe167bbd330237d01294959745497f463751d59 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 21 Apr 2021 12:12:22 +0100 Subject: [PATCH 02/10] google auth E2E --- packages/auth/src/db/utils.js | 9 ++- packages/auth/src/index.js | 9 +-- .../auth/src/middleware/passport/google.js | 52 ++++++++++++--- .../src/components/login/LoginForm.svelte | 1 + .../src/api/controllers/admin/configs.js | 38 +++++++++-- packages/worker/src/api/controllers/auth.js | 66 +++++++++++++++++-- .../worker/src/api/routes/admin/configs.js | 3 +- packages/worker/src/api/routes/auth.js | 12 ++-- 8 files changed, 155 insertions(+), 35 deletions(-) diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 32034bd731..90b44c43b7 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -70,12 +70,11 @@ exports.getUserParams = (email = "", otherProps = {}) => { * Generates a new configuration ID. * @returns {string} The new configuration ID which the config doc can be stored under. */ -exports.generateConfigID = (type = "", group = "") => { - group += SEPARATOR +exports.generateConfigID = (type = "", group = "", user = "") => { + // group += SEPARATOR + const scope = [type, group, user].join(SEPARATOR) - return `${ - DocumentTypes.CONFIG - }${SEPARATOR}${type}${SEPARATOR}${group}${newid()}` + return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${newid()}` } /** diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 4c6ece1b62..4ef30c9f02 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -1,10 +1,10 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -// const GoogleStrategy = require("passport-google-oauth").Strategy +const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const database = require("./db") -const { StaticDatabases } = require("./db/utils") -const { jwt, local, authenticated } = require("./middleware") +const { StaticDatabases, DocumentTypes } = require("./db/utils") +const { jwt, local, google, authenticated } = require("./middleware") const { Cookies, UserStatus } = require("./constants") const { hash, compare } = require("./hashing") const { @@ -27,7 +27,7 @@ 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.use(new GoogleStrategy(google.options, google.authenticate)) passport.serializeUser((user, done) => done(null, user)) @@ -50,6 +50,7 @@ module.exports = { passport, Cookies, UserStatus, + DocumentTypes, StaticDatabases, generateUserID, getUserParams, diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index 6246e5e768..9113fba1cf 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -1,18 +1,54 @@ const env = require("../../environment") +const jwt = require("jsonwebtoken") +const database = require("../../db") +const { StaticDatabases, generateUserID } = require("../../db/utils") exports.options = { - clientId: env.GOOGLE_CLIENT_ID, + clientID: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, callbackURL: env.GOOGLE_AUTH_CALLBACK_URL, } exports.authenticate = async function(token, tokenSecret, profile, done) { - console.log({ - token, - tokenSecret, - profile, - done, + if (!profile._json.email) return done(null, false, "Email Required.") + + // Check the user exists in the instance DB by email + const db = new database.CouchDB(StaticDatabases.GLOBAL.name) + + let dbUser + const userId = generateUserID(profile._json.email) + + try { + // use the google profile id + dbUser = await db.get(userId) + } catch (err) { + console.error("Google user not found. Creating..") + // create the user + const user = { + _id: userId, + provider: profile.provider, + roles: {}, + builder: { + global: true, + }, + ...profile._json, + } + const response = await db.post(user) + + dbUser = user + dbUser._rev = response.rev + } + + // authenticate + const payload = { + userId: dbUser._id, + builder: dbUser.builder, + email: dbUser.email, + } + + dbUser.token = jwt.sign(payload, env.JWT_SECRET, { + expiresIn: "1 day", }) - // retrieve user ... - // fetchUser().then(user => done(null, user)) + + return done(null, dbUser) } diff --git a/packages/builder/src/components/login/LoginForm.svelte b/packages/builder/src/components/login/LoginForm.svelte index 7e32efb7c5..55d1ee3bf5 100644 --- a/packages/builder/src/components/login/LoginForm.svelte +++ b/packages/builder/src/components/login/LoginForm.svelte @@ -46,6 +46,7 @@ + Sign In with Google diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js index e9fc8a3942..10f1d2cf2b 100644 --- a/packages/worker/src/api/controllers/admin/configs.js +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -1,17 +1,47 @@ const CouchDB = require("../../../db") -const { StaticDatabases } = require("@budibase/auth") -const { generateConfigID } = require("@budibase/auth") -const { getConfigParams } = require("@budibase/auth/src/db/utils") +const { StaticDatabases, DocumentTypes } = 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 // Config does not exist yet if (!configDoc._id) { - configDoc._id = generateConfigID(configDoc.type, configDoc.group) + configDoc._id = generateConfigID( + configDoc.type, + configDoc.group, + configDoc.user + ) } try { diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index 03589ab457..d2aaf552e0 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -1,4 +1,38 @@ -const { passport, Cookies, clearCookie } = require("@budibase/auth") +const { + passport, + Cookies, + StaticDatabases, + clearCookie, +} = require("@budibase/auth") +const CouchDB = require("../../db") + +const GLOBAL_DB = StaticDatabases.GLOBAL.name + +async function setToken(ctx) { + return async function(err, user) { + if (err) { + return ctx.throw(403, "Unauthorized") + } + + const expires = new Date() + expires.setDate(expires.getDate() + 1) + + if (!user) { + return ctx.throw(403, "Unauthorized") + } + + ctx.cookies.set(Cookies.Auth, user.token, { + expires, + path: "/", + httpOnly: false, + overwrite: true, + }) + + delete user.token + + ctx.body = { user } + } +} exports.authenticate = async (ctx, next) => { return passport.authenticate("local", async (err, user) => { @@ -31,10 +65,30 @@ exports.logout = async ctx => { ctx.body = { message: "User logged out" } } -exports.googleAuth = async () => { - // return passport.authenticate("google") -} +exports.googleAuth = async (ctx, next) => { + return passport.authenticate( + "google", + { successRedirect: "/", failureRedirect: "/" }, + async (err, user) => { + if (err) { + return ctx.throw(403, "Unauthorized") + } -exports.googleAuth = async () => { - // return passport.authenticate("google") + const expires = new Date() + expires.setDate(expires.getDate() + 1) + + if (!user) { + return ctx.throw(403, "Unauthorized") + } + + ctx.cookies.set(Cookies.Auth, user.token, { + expires, + path: "/", + httpOnly: false, + overwrite: true, + }) + + ctx.redirect("/") + } + )(ctx, next) } diff --git a/packages/worker/src/api/routes/admin/configs.js b/packages/worker/src/api/routes/admin/configs.js index 5b7354bfc3..0399026cf2 100644 --- a/packages/worker/src/api/routes/admin/configs.js +++ b/packages/worker/src/api/routes/admin/configs.js @@ -10,7 +10,7 @@ const router = Router() function buildConfigSaveValidation() { // prettier-ignore return joiValidator.body(Joi.object({ - type: Joi.string().valid(...Object.values(Configs)).required() + type: Joi.string().valid(...Object.values(Configs)).required(), }).required().unknown(true)) } @@ -21,6 +21,7 @@ router authenticated, controller.save ) + .post("/api/admin/config/status", controller.configStatus) .delete("/api/admin/configs/:id", authenticated, controller.destroy) .get("/api/admin/configs", authenticated, controller.fetch) .get("/api/admin/configs/:id", authenticated, controller.find) diff --git a/packages/worker/src/api/routes/auth.js b/packages/worker/src/api/routes/auth.js index deea678c63..ac87ef977a 100644 --- a/packages/worker/src/api/routes/auth.js +++ b/packages/worker/src/api/routes/auth.js @@ -1,19 +1,17 @@ const Router = require("@koa/router") const { passport } = require("@budibase/auth") const authController = require("../controllers/auth") +const context = require("koa/lib/context") const router = Router() router .post("/api/admin/auth", authController.authenticate) - .post("/api/admin/auth/logout", authController.logout) - .get("/api/auth/google", passport.authenticate("google")) .get( - "/api/auth/google/callback", - passport.authenticate("google", { - successRedirect: "/app", - failureRedirect: "/", - }) + "/api/admin/auth/google", + passport.authenticate("google", { scope: ["profile", "email"] }) ) + .get("/api/admin/auth/google/callback", authController.googleAuth) + .post("/api/admin/auth/logout", authController.logout) module.exports = router From 28f8f8b6efe91cc4d72e328ab62ed3c08cb39c21 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 21 Apr 2021 18:40:32 +0100 Subject: [PATCH 03/10] custom google middleware --- packages/auth/src/index.js | 3 +- .../auth/src/middleware/passport/google.js | 14 +- packages/worker/.vscode/launch.json | 139 ++++++++++++++++++ .../src/api/controllers/admin/configs.js | 7 + packages/worker/src/api/controllers/auth.js | 8 + 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 packages/worker/.vscode/launch.json diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 4ef30c9f02..d1b09c3ae0 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -1,7 +1,6 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const database = require("./db") const { StaticDatabases, DocumentTypes } = require("./db/utils") const { jwt, local, google, authenticated } = require("./middleware") @@ -27,7 +26,7 @@ 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.use(new GoogleStrategy(google.options, google.authenticate)) passport.serializeUser((user, done) => done(null, user)) diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index 9113fba1cf..69c038638f 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -1,6 +1,7 @@ const env = require("../../environment") const jwt = require("jsonwebtoken") const database = require("../../db") +const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const { StaticDatabases, generateUserID } = require("../../db/utils") exports.options = { @@ -9,7 +10,7 @@ exports.options = { callbackURL: env.GOOGLE_AUTH_CALLBACK_URL, } -exports.authenticate = async function(token, tokenSecret, profile, done) { +async function authenticate(token, tokenSecret, profile, done) { if (!profile._json.email) return done(null, false, "Email Required.") // Check the user exists in the instance DB by email @@ -52,3 +53,14 @@ exports.authenticate = async function(token, tokenSecret, profile, done) { return done(null, dbUser) } + +exports.CustomGoogleStrategy = function(config) { + return new GoogleStrategy( + { + clientID: config.clientID, + clientSecret: config.clientSecret, + callbackURL: config.callbackURL, + }, + authenticate + ) +} diff --git a/packages/worker/.vscode/launch.json b/packages/worker/.vscode/launch.json new file mode 100644 index 0000000000..7417938376 --- /dev/null +++ b/packages/worker/.vscode/launch.json @@ -0,0 +1,139 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Start Server", + "program": "${workspaceFolder}/src/index.js" + }, + { + "type": "node", + "request": "launch", + "name": "Jest - All", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest - Users", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["user.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest - Instances", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["instance.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest - Roles", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["role.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest - Records", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["record.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest - Models", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["table.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest - Views", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["view.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest - Applications", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["application.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest Builder", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["builder", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Initialise Budibase", + "program": "yarn", + "args": ["run", "initialise"], + "console": "externalTerminal" + } + ] +} diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js index 10f1d2cf2b..c28f0179ee 100644 --- a/packages/worker/src/api/controllers/admin/configs.js +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -69,6 +69,13 @@ exports.fetch = async function(ctx) { 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 try { const record = await db.get(ctx.params.id) ctx.body = record diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index d2aaf552e0..df759ffc84 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -65,6 +65,14 @@ exports.logout = async ctx => { ctx.body = { message: "User logged out" } } +// exports.googleAuth = async (ctx, next) => +// passport.authenticate( +// "google", +// { successRedirect: "/", failureRedirect: "/" }, +// (ctx +// setToken(ctx, next) +// ) + exports.googleAuth = async (ctx, next) => { return passport.authenticate( "google", From 8fab374c1fce4e515253b198776caaa75fdd6147 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 21 Apr 2021 21:08:04 +0100 Subject: [PATCH 04/10] google login reading from couch --- packages/auth/src/db/utils.js | 4 +- packages/auth/src/index.js | 6 ++- .../auth/src/middleware/passport/google.js | 54 ++++++++++++++----- .../src/components/login/LoginForm.svelte | 2 +- packages/worker/src/api/controllers/auth.js | 23 ++++---- .../worker/src/api/routes/admin/configs.js | 15 ++---- .../worker/src/api/routes/admin/templates.js | 6 +-- packages/worker/src/api/routes/auth.js | 7 +-- packages/worker/src/constants/index.js | 1 + 9 files changed, 69 insertions(+), 49 deletions(-) diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 9d366c30a6..ab9142eaee 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -48,8 +48,8 @@ exports.getGroupParams = (id = "", otherProps = {}) => { * Generates a new global user ID. * @returns {string} The new user ID which the user doc can be stored under. */ -exports.generateGlobalUserID = () => { - return `${DocumentTypes.USER}${SEPARATOR}${newid()}` +exports.generateGlobalUserID = id => { + return `${DocumentTypes.USER}${SEPARATOR}${id || newid()}` } /** diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 3dcf26d346..1906e20be2 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -1,7 +1,7 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -const database = require("./db") +const constants = require("./constants") const { StaticDatabases, DocumentTypes } = require("./db/utils") const { jwt, local, google, authenticated } = require("./middleware") const { Cookies, UserStatus } = require("./constants") @@ -55,7 +55,11 @@ module.exports = { auth: { buildAuthMiddleware: authenticated, passport, + middlewares: { + google, + }, }, + constants, passport, Cookies, UserStatus, diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index 69c038638f..ad7c83d189 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -2,7 +2,11 @@ const env = require("../../environment") const jwt = require("jsonwebtoken") const database = require("../../db") const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy -const { StaticDatabases, generateUserID } = require("../../db/utils") +const { + StaticDatabases, + generateUserID, + generateGlobalUserID, +} = require("../../db/utils") exports.options = { clientID: env.GOOGLE_CLIENT_ID, @@ -11,13 +15,11 @@ exports.options = { } async function authenticate(token, tokenSecret, profile, done) { - if (!profile._json.email) return done(null, false, "Email Required.") - // Check the user exists in the instance DB by email - const db = new database.CouchDB(StaticDatabases.GLOBAL.name) + const db = database.getDB(StaticDatabases.GLOBAL.name) let dbUser - const userId = generateUserID(profile._json.email) + const userId = generateGlobalUserID(profile.id) try { // use the google profile id @@ -54,13 +56,37 @@ async function authenticate(token, tokenSecret, profile, done) { return done(null, dbUser) } -exports.CustomGoogleStrategy = function(config) { - return new GoogleStrategy( - { - clientID: config.clientID, - clientSecret: config.clientSecret, - callbackURL: config.callbackURL, - }, - authenticate - ) +/** + * 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 + */ +exports.strategyFactory = async function() { + try { + const db = database.getDB(StaticDatabases.GLOBAL.name) + + const config = await db.get( + "config_google__767bd8f363854dfa8752f593a637b3fd" + ) + + const { clientID, clientSecret, callbackURL } = config + + if (!clientID || !clientSecret || !callbackURL) { + throw new Error( + "Configuration invalid. Must contain google clientID, clientSecret and callbackURL" + ) + } + + return new GoogleStrategy( + { + clientID: config.clientID, + clientSecret: config.clientSecret, + callbackURL: config.callbackURL, + }, + authenticate + ) + } catch (err) { + console.error(err) + throw new Error("Error constructing google authentication strategy", err) + } } diff --git a/packages/builder/src/components/login/LoginForm.svelte b/packages/builder/src/components/login/LoginForm.svelte index 30d903a9d4..888054df1b 100644 --- a/packages/builder/src/components/login/LoginForm.svelte +++ b/packages/builder/src/components/login/LoginForm.svelte @@ -39,7 +39,7 @@ - Sign In with Google + Sign In With Google diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index 05e3256e34..bfc331042e 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -1,6 +1,7 @@ const authPkg = require("@budibase/auth") +const { google } = require("@budibase/auth/src/middleware") const { clearCookie } = authPkg.utils -const { Cookies } = authPkg.constants +const { Cookies } = authPkg const { passport } = authPkg.auth exports.authenticate = async (ctx, next) => { @@ -34,18 +35,20 @@ exports.logout = async ctx => { ctx.body = { message: "User logged out" } } -// exports.googleAuth = async (ctx, next) => -// passport.authenticate( -// "google", -// { successRedirect: "/", failureRedirect: "/" }, -// (ctx -// setToken(ctx, next) -// ) +exports.googlePreAuth = async (ctx, next) => { + const strategy = await google.strategyFactory() + + return passport.authenticate(strategy, { + scope: ["profile", "email"], + })(ctx, next) +} exports.googleAuth = async (ctx, next) => { + const strategy = await google.strategyFactory() + return passport.authenticate( - "google", - { successRedirect: "/", failureRedirect: "/" }, + strategy, + { successRedirect: "/", failureRedirect: "/error" }, async (err, user) => { if (err) { return ctx.throw(403, "Unauthorized") diff --git a/packages/worker/src/api/routes/admin/configs.js b/packages/worker/src/api/routes/admin/configs.js index 0399026cf2..0411a9ffa0 100644 --- a/packages/worker/src/api/routes/admin/configs.js +++ b/packages/worker/src/api/routes/admin/configs.js @@ -15,15 +15,10 @@ function buildConfigSaveValidation() { } router - .post( - "/api/admin/configs", - buildConfigSaveValidation(), - authenticated, - controller.save - ) - .post("/api/admin/config/status", controller.configStatus) - .delete("/api/admin/configs/:id", authenticated, controller.destroy) - .get("/api/admin/configs", authenticated, controller.fetch) - .get("/api/admin/configs/:id", authenticated, controller.find) + .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) module.exports = router diff --git a/packages/worker/src/api/routes/admin/templates.js b/packages/worker/src/api/routes/admin/templates.js index 756b3e7cf0..2207b72458 100644 --- a/packages/worker/src/api/routes/admin/templates.js +++ b/packages/worker/src/api/routes/admin/templates.js @@ -21,11 +21,7 @@ function buildTemplateSaveValidation() { router .get("/api/admin/template/definitions", controller.definitions) - .post( - "/api/admin/template", - buildTemplateSaveValidation(), - controller.save - ) + .post("/api/admin/template", buildTemplateSaveValidation(), controller.save) .get("/api/admin/template", controller.fetch) .get("/api/admin/template/:type", controller.fetchByType) .get("/api/admin/template/:ownerId", controller.fetchByOwner) diff --git a/packages/worker/src/api/routes/auth.js b/packages/worker/src/api/routes/auth.js index ac7c7c0737..72fddec399 100644 --- a/packages/worker/src/api/routes/auth.js +++ b/packages/worker/src/api/routes/auth.js @@ -1,16 +1,11 @@ const Router = require("@koa/router") -const { passport } = require("@budibase/auth").auth const authController = require("../controllers/auth") -const context = require("koa/lib/context") const router = Router() router .post("/api/admin/auth", authController.authenticate) - .get( - "/api/admin/auth/google", - passport.authenticate("google", { scope: ["profile", "email"] }) - ) + .get("/api/admin/auth/google", authController.googlePreAuth) .get("/api/admin/auth/google/callback", authController.googleAuth) .post("/api/admin/auth/logout", authController.logout) diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js index 345094206b..5d52ce798f 100644 --- a/packages/worker/src/constants/index.js +++ b/packages/worker/src/constants/index.js @@ -11,6 +11,7 @@ exports.Configs = { SETTINGS: "settings", ACCOUNT: "account", SMTP: "smtp", + GOOGLE: "google", } exports.TemplateTypes = { From 64628481918ade34a8aefe6eeffbd7ee402d7e0b Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 22 Apr 2021 11:45:22 +0100 Subject: [PATCH 05/10] 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 From f7085a57c7d3e7d99957fca61f7a4027c5bfeb26 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 22 Apr 2021 11:48:37 +0100 Subject: [PATCH 06/10] lint --- packages/worker/src/api/controllers/admin/templates.js | 8 ++++++-- packages/worker/src/api/controllers/auth.js | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/worker/src/api/controllers/admin/templates.js b/packages/worker/src/api/controllers/admin/templates.js index 25360bd1e5..2b4f6f8284 100644 --- a/packages/worker/src/api/controllers/admin/templates.js +++ b/packages/worker/src/api/controllers/admin/templates.js @@ -1,4 +1,8 @@ -const { generateTemplateID, getTemplateParams, StaticDatabases } = require("@budibase/auth").db +const { + generateTemplateID, + getTemplateParams, + StaticDatabases, +} = require("@budibase/auth").db const { CouchDB } = require("../../../db") const { TemplatePurposePretty } = require("../../../constants") @@ -42,7 +46,7 @@ exports.save = async ctx => { exports.definitions = async ctx => { ctx.body = { - purpose: TemplatePurposePretty + purpose: TemplatePurposePretty, } } diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index 61025b1a48..f9e5e212de 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -53,7 +53,11 @@ exports.googlePreAuth = async (ctx, next) => { } exports.googleAuth = async (ctx, next) => { - const strategy = await google.strategyFactory(ctx) + const strategy = await google.strategyFactory({ + type: Configs.GOOGLE, + user: ctx.user._id, + group: ctx.query.group, + }) return passport.authenticate( strategy, From 2555d711b23d5045e2096eba6042b6f26b3aae8a Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 22 Apr 2021 13:46:54 +0100 Subject: [PATCH 07/10] scoped configuration management --- packages/auth/src/db/utils.js | 49 ++++++++++++++++++- packages/auth/src/index.js | 2 + .../auth/src/middleware/passport/google.js | 6 +-- .../src/api/controllers/admin/configs.js | 48 ++---------------- .../src/api/controllers/admin/templates.js | 2 +- packages/worker/src/api/controllers/auth.js | 15 ++++-- 6 files changed, 67 insertions(+), 55 deletions(-) diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 408daf7dd4..2f34bc0c51 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -98,7 +98,7 @@ 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 }) => { +const generateConfigID = ({ type, group, user }) => { const scope = [type, group, user].filter(Boolean).join(SEPARATOR) return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}` @@ -107,7 +107,7 @@ exports.generateConfigID = ({ type, group, user }) => { /** * Gets parameters for retrieving configurations. */ -exports.getConfigParams = ({ type, group, user }, otherProps = {}) => { +const getConfigParams = ({ type, group, user }, otherProps = {}) => { const scope = [type, group, user].filter(Boolean).join(SEPARATOR) return { @@ -116,3 +116,48 @@ exports.getConfigParams = ({ type, group, user }, otherProps = {}) => { endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`, } } + +/** + * Returns the most granular configuration document from the DB based on the type, group and userID passed. + * @param {*} db - db instance to quer + * @param {Object} scopes - the type, group and userID scopes of the configuration. + * @returns The most granular configuration document based on the scope. + */ +const determineScopedConfig = async function(db, { type, user, group }) { + const response = await db.allDocs( + getConfigParams( + { type, user, 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, user, group }))) { + return config + } + + // Config is specific to a user + if (config._id.includes(generateConfigID({ type, user }))) { + return config + } + + // Config is specific to a group only + if (config._id.includes(generateConfigID({ type, group }))) { + return config + } + + // Config specific to a config type only + return config + }) + + return scopedConfig +} + +exports.generateConfigID = generateConfigID +exports.getConfigParams = getConfigParams +exports.determineScopedConfig = determineScopedConfig diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index bdc9e16609..4149314b4d 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -22,6 +22,7 @@ const { getEmailFromUserID, generateConfigID, getConfigParams, + determineScopedConfig, } = require("./db/utils") // Strategies @@ -71,6 +72,7 @@ module.exports = { getEmailFromUserID, generateConfigID, getConfigParams, + determineScopedConfig, hash, compare, getAppId, diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index ea49b2c35c..968dfa3e93 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -51,12 +51,8 @@ async function authenticate(token, tokenSecret, profile, done) { * 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(scope) { +exports.strategyFactory = async function(config) { try { - const db = database.getDB(StaticDatabases.GLOBAL.name) - - const config = await db.get(scope) - const { clientID, clientSecret, callbackURL } = config if (!clientID || !clientSecret || !callbackURL) { diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js index 5e39ebefc0..08c2b6df7d 100644 --- a/packages/worker/src/api/controllers/admin/configs.js +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -1,5 +1,5 @@ const CouchDB = require("../../../db") -const { StaticDatabases } = require("@budibase/auth") +const { StaticDatabases, determineScopedConfig } = require("@budibase/auth") const { generateConfigID, getConfigParams } = require("@budibase/auth") const GLOBAL_DB = StaticDatabases.GLOBAL.name @@ -59,49 +59,11 @@ exports.find = async function(ctx) { } try { - 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 + const scopedConfig = await determineScopedConfig(db, { + type: ctx.params.type, + user: userId, + group, }) if (scopedConfig) { diff --git a/packages/worker/src/api/controllers/admin/templates.js b/packages/worker/src/api/controllers/admin/templates.js index 2b4f6f8284..d91323e0a1 100644 --- a/packages/worker/src/api/controllers/admin/templates.js +++ b/packages/worker/src/api/controllers/admin/templates.js @@ -74,6 +74,6 @@ exports.find = async ctx => { exports.destroy = async ctx => { // TODO - const db = new CouchDB(GLOBAL_DB) + // const db = new CouchDB(GLOBAL_DB) ctx.body = {} } diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index f9e5e212de..a58a7abdab 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -1,10 +1,14 @@ +const { determineScopedConfig } = require("@budibase/auth") const authPkg = require("@budibase/auth") const { google } = require("@budibase/auth/src/middleware") const { Configs } = require("../../constants") +const CouchDB = require("../../db") const { clearCookie } = authPkg.utils const { Cookies } = authPkg const { passport } = authPkg.auth +const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name + exports.authenticate = async (ctx, next) => { return passport.authenticate("local", async (err, user) => { if (err) { @@ -41,11 +45,12 @@ exports.logout = async ctx => { * On a successful login, you will be redirected to the googleAuth callback route. */ exports.googlePreAuth = async (ctx, next) => { - const strategy = await google.strategyFactory({ + const db = new CouchDB(GLOBAL_DB) + const config = await determineScopedConfig(db, { type: Configs.GOOGLE, - user: ctx.user._id, group: ctx.query.group, }) + const strategy = await google.strategyFactory(config) return passport.authenticate(strategy, { scope: ["profile", "email"], @@ -53,11 +58,13 @@ exports.googlePreAuth = async (ctx, next) => { } exports.googleAuth = async (ctx, next) => { - const strategy = await google.strategyFactory({ + const db = new CouchDB(GLOBAL_DB) + + const config = await determineScopedConfig(db, { type: Configs.GOOGLE, - user: ctx.user._id, group: ctx.query.group, }) + const strategy = await google.strategyFactory(config) return passport.authenticate( strategy, From a071d7b365eb448952fe0fb111382cf614b8914d Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 22 Apr 2021 14:07:00 +0100 Subject: [PATCH 08/10] tidy up --- packages/auth/src/db/utils.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 2f34bc0c51..d80d1f0662 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -132,29 +132,28 @@ const determineScopedConfig = async function(db, { type, user, group }) { } ) ) - const configs = response.rows.map(row => row.doc) + const configs = response.rows.map(row => { + const config = 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, user, group }))) { - return config + config.score = 4 + } else if (config._id.includes(generateConfigID({ type, user }))) { + // Config is specific to a user only + config.score = 3 + } else if (config._id.includes(generateConfigID({ type, group }))) { + // Config is specific to a group only + config.score = 2 + } else if (config._id.includes(generateConfigID({ type }))) { + // Config is specific to a type only + config.score = 1 } - - // Config is specific to a user - if (config._id.includes(generateConfigID({ type, user }))) { - return config - } - - // Config is specific to a group only - if (config._id.includes(generateConfigID({ type, group }))) { - return config - } - - // Config specific to a config type only return config }) + // Find the config with the most granular scope based on context + const scopedConfig = configs.sort((a, b) => b.score - a.score)[0] + return scopedConfig } From 9fdff36b54ec279c99d931c7569b7d133b5ab266 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 22 Apr 2021 14:53:19 +0100 Subject: [PATCH 09/10] only keep userId in payload --- packages/auth/src/db/utils.js | 2 +- packages/auth/src/middleware/passport/local.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index d80d1f0662..393e03e492 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -119,7 +119,7 @@ const getConfigParams = ({ type, group, user }, otherProps = {}) => { /** * Returns the most granular configuration document from the DB based on the type, group and userID passed. - * @param {*} db - db instance to quer + * @param {Object} db - db instance to query * @param {Object} scopes - the type, group and userID scopes of the configuration. * @returns The most granular configuration document based on the scope. */ diff --git a/packages/auth/src/middleware/passport/local.js b/packages/auth/src/middleware/passport/local.js index 1942d0c424..5b8bf307d7 100644 --- a/packages/auth/src/middleware/passport/local.js +++ b/packages/auth/src/middleware/passport/local.js @@ -33,8 +33,6 @@ exports.authenticate = async function(email, password, done) { if (await compare(password, dbUser.password)) { const payload = { userId: dbUser._id, - builder: dbUser.builder, - email: dbUser.email, } dbUser.token = jwt.sign(payload, env.JWT_SECRET, { From c273152126bbf8147648925e8a02135ecd16e2de Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 22 Apr 2021 15:27:09 +0100 Subject: [PATCH 10/10] fix imports --- packages/auth/src/index.js | 50 ++----------------- .../src/api/controllers/admin/configs.js | 10 ++-- packages/worker/src/api/controllers/auth.js | 7 ++- 3 files changed, 12 insertions(+), 55 deletions(-) diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 4149314b4d..c1e0a08242 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -1,29 +1,9 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -const constants = require("./constants") -const { StaticDatabases, DocumentTypes } = require("./db/utils") -const { jwt, local, google, authenticated } = require("./middleware") -const { Cookies, UserStatus } = require("./constants") -const { hash, compare } = require("./hashing") -const { - getAppId, - setCookie, - getCookie, - clearCookie, - isClient, -} = require("./utils") +const { StaticDatabases } = require("./db/utils") +const { jwt, local, authenticated, google } = require("./middleware") const { setDB, getDB } = require("./db") -const { - generateUserID, - getUserParams, - generateGroupID, - getGroupParams, - getEmailFromUserID, - generateConfigID, - getConfigParams, - determineScopedConfig, -} = require("./db/utils") // Strategies passport.use(new LocalStrategy(local.options, local.authenticate)) @@ -55,30 +35,8 @@ module.exports = { auth: { buildAuthMiddleware: authenticated, passport, - middlewares: { - google, - }, + google, }, - constants, - passport, - Cookies, - UserStatus, - DocumentTypes, StaticDatabases, - generateUserID, - getUserParams, - generateGroupID, - getGroupParams, - getEmailFromUserID, - generateConfigID, - getConfigParams, - determineScopedConfig, - hash, - compare, - getAppId, - setCookie, - getCookie, - clearCookie, - authenticated, - isClient, + constants: require("./constants"), } diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js index 08c2b6df7d..67f3405fa4 100644 --- a/packages/worker/src/api/controllers/admin/configs.js +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -1,6 +1,6 @@ const CouchDB = require("../../../db") -const { StaticDatabases, determineScopedConfig } = require("@budibase/auth") -const { generateConfigID, getConfigParams } = require("@budibase/auth") +const authPkg = require("@budibase/auth") +const { utils, StaticDatabases } = authPkg const GLOBAL_DB = StaticDatabases.GLOBAL.name @@ -11,7 +11,7 @@ exports.save = async function(ctx) { // Config does not exist yet if (!configDoc._id) { - configDoc._id = generateConfigID({ + configDoc._id = utils.generateConfigID({ type, group, user, @@ -33,7 +33,7 @@ exports.save = async function(ctx) { exports.fetch = async function(ctx) { const db = new CouchDB(GLOBAL_DB) const response = await db.allDocs( - getConfigParams(undefined, { + utils.getConfigParams(undefined, { include_docs: true, }) ) @@ -60,7 +60,7 @@ exports.find = async function(ctx) { try { // Find the config with the most granular scope based on context - const scopedConfig = await determineScopedConfig(db, { + const scopedConfig = await authPkg.db.determineScopedConfig(db, { type: ctx.params.type, user: userId, group, diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index a58a7abdab..bcda523a93 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -1,10 +1,9 @@ -const { determineScopedConfig } = require("@budibase/auth") const authPkg = require("@budibase/auth") const { google } = require("@budibase/auth/src/middleware") const { Configs } = require("../../constants") const CouchDB = require("../../db") const { clearCookie } = authPkg.utils -const { Cookies } = authPkg +const { Cookies } = authPkg.constants const { passport } = authPkg.auth const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name @@ -46,7 +45,7 @@ exports.logout = async ctx => { */ exports.googlePreAuth = async (ctx, next) => { const db = new CouchDB(GLOBAL_DB) - const config = await determineScopedConfig(db, { + const config = await authPkg.db.determineScopedConfig(db, { type: Configs.GOOGLE, group: ctx.query.group, }) @@ -60,7 +59,7 @@ exports.googlePreAuth = async (ctx, next) => { exports.googleAuth = async (ctx, next) => { const db = new CouchDB(GLOBAL_DB) - const config = await determineScopedConfig(db, { + const config = await authPkg.db.determineScopedConfig(db, { type: Configs.GOOGLE, group: ctx.query.group, })