config specificity

This commit is contained in:
Martin McKeaveney 2021-04-22 11:45:22 +01:00
parent 8fab374c1f
commit 6462848191
10 changed files with 107 additions and 80 deletions

View File

@ -98,20 +98,21 @@ exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
* Generates a new configuration ID. * Generates a new configuration ID.
* @returns {string} The new configuration ID which the config doc can be stored under. * @returns {string} The new configuration ID which the config doc can be stored under.
*/ */
exports.generateConfigID = (type = "", group = "", user = "") => { exports.generateConfigID = ({ type, group, user }) => {
// group += SEPARATOR const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
const scope = [type, group, user].join(SEPARATOR)
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${newid()}` return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
} }
/** /**
* Gets parameters for retrieving configurations. * 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 { return {
...otherProps, ...otherProps,
startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${type}${SEPARATOR}${group}`, startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`,
endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${type}${SEPARATOR}${group}${UNICODE_MAX}`, endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`,
} }
} }

View File

@ -2,7 +2,4 @@ module.exports = {
JWT_SECRET: process.env.JWT_SECRET, JWT_SECRET: process.env.JWT_SECRET,
COUCH_DB_URL: process.env.COUCH_DB_URL, COUCH_DB_URL: process.env.COUCH_DB_URL,
SALT_ROUNDS: process.env.SALT_ROUNDS, 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,
} }

View File

@ -27,7 +27,6 @@ const {
// Strategies // Strategies
passport.use(new LocalStrategy(local.options, local.authenticate)) passport.use(new LocalStrategy(local.options, local.authenticate))
passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
// passport.use(new GoogleStrategy(google.options, google.authenticate))
passport.serializeUser((user, done) => done(null, user)) passport.serializeUser((user, done) => done(null, user))

View File

@ -1,5 +1,7 @@
const { Cookies } = require("../constants") const { Cookies } = require("../constants")
const database = require("../db")
const { getCookie } = require("../utils") const { getCookie } = require("../utils")
const { StaticDatabases } = require("../db/utils")
module.exports = (noAuthPatterns = []) => { module.exports = (noAuthPatterns = []) => {
const regex = new RegExp(noAuthPatterns.join("|")) const regex = new RegExp(noAuthPatterns.join("|"))
@ -13,8 +15,11 @@ module.exports = (noAuthPatterns = []) => {
const authCookie = getCookie(ctx, Cookies.Auth) const authCookie = getCookie(ctx, Cookies.Auth)
if (authCookie) { if (authCookie) {
const db = database.getDB(StaticDatabases.GLOBAL.name)
const user = await db.get(authCookie.userId)
delete user.password
ctx.isAuthenticated = true ctx.isAuthenticated = true
ctx.user = authCookie ctx.user = user
} }
return next() return next()

View File

@ -2,17 +2,7 @@ const env = require("../../environment")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const database = require("../../db") const database = require("../../db")
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
const { const { StaticDatabases, generateGlobalUserID } = require("../../db/utils")
StaticDatabases,
generateUserID,
generateGlobalUserID,
} = require("../../db/utils")
exports.options = {
clientID: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
callbackURL: env.GOOGLE_AUTH_CALLBACK_URL,
}
async function authenticate(token, tokenSecret, profile, done) { async function authenticate(token, tokenSecret, profile, done) {
// Check the user exists in the instance DB by email // 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 * 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. * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Passport Google Strategy * @returns Dynamically configured Passport Google Strategy
*/ */
exports.strategyFactory = async function() { exports.strategyFactory = async function(scope) {
try { try {
const db = database.getDB(StaticDatabases.GLOBAL.name) const db = database.getDB(StaticDatabases.GLOBAL.name)
const config = await db.get( const config = await db.get(scope)
"config_google__767bd8f363854dfa8752f593a637b3fd"
)
const { clientID, clientSecret, callbackURL } = config const { clientID, clientSecret, callbackURL } = config

View File

@ -72,7 +72,7 @@ exports.createMetadata = async function(ctx) {
exports.updateSelfMetadata = async function(ctx) { exports.updateSelfMetadata = async function(ctx) {
// overwrite the ID with current users // 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 // make sure no stale rev
delete ctx.request.body._rev delete ctx.request.body._rev
await exports.updateMetadata(ctx) await exports.updateMetadata(ctx)

View File

@ -31,7 +31,7 @@ module.exports = async (ctx, next) => {
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC) appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC)
) { ) {
// Different App ID means cookie needs reset, or if the same public user has logged in // 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) const globalUser = await getGlobalUsers(ctx, requestAppId, globalId)
updateCookie = true updateCookie = true
appId = requestAppId appId = requestAppId
@ -50,7 +50,7 @@ module.exports = async (ctx, next) => {
ctx.appId = appId ctx.appId = appId
if (roleId) { if (roleId) {
ctx.roleId = 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 = {
...ctx.user, ...ctx.user,
// override userID with metadata one // override userID with metadata one

View File

@ -1,53 +1,27 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const { StaticDatabases, DocumentTypes } = require("@budibase/auth") const { StaticDatabases } = require("@budibase/auth")
const { generateConfigID, getConfigParams } = 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 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) { exports.save = async function(ctx) {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const configDoc = ctx.request.body const configDoc = ctx.request.body
const { type, group, user } = configDoc
// Config does not exist yet // Config does not exist yet
if (!configDoc._id) { if (!configDoc._id) {
configDoc._id = generateConfigID( configDoc._id = generateConfigID({
configDoc.type, type,
configDoc.group, group,
configDoc.user user,
) })
} }
try { try {
const response = await db.post(configDoc) const response = await db.post(configDoc)
ctx.body = { ctx.body = {
type: configDoc.type, type,
_id: response.id, _id: response.id,
_rev: response.rev, _rev: response.rev,
} }
@ -67,18 +41,74 @@ exports.fetch = async function(ctx) {
ctx.body = groups ctx.body = groups
} }
/**
* Gets the most granular config for a particular configuration type.
* The hierarchy is type -> group -> user.
*/
exports.find = async function(ctx) { exports.find = async function(ctx) {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const response = await db.allDocs( const userId = ctx.params.user && ctx.params.user._id
getConfigParams(undefined, {
include_docs: true, const { group } = ctx.query
}) if (group) {
) const group = await db.get(group)
const groups = response.rows.map(row => row.doc) const userInGroup = group.users.some(groupUser => groupUser === userId)
ctx.body = groups if (!ctx.user.admin && !userInGroup) {
ctx.throw(400, `User is not in specified group: ${group}.`)
}
}
try { try {
const record = await db.get(ctx.params.id) const response = await db.allDocs(
ctx.body = record 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) { } catch (err) {
ctx.throw(err.status, err) ctx.throw(err.status, err)
} }

View File

@ -1,5 +1,6 @@
const authPkg = require("@budibase/auth") const authPkg = require("@budibase/auth")
const { google } = require("@budibase/auth/src/middleware") const { google } = require("@budibase/auth/src/middleware")
const { Configs } = require("../../constants")
const { clearCookie } = authPkg.utils const { clearCookie } = authPkg.utils
const { Cookies } = authPkg const { Cookies } = authPkg
const { passport } = authPkg.auth const { passport } = authPkg.auth
@ -35,8 +36,16 @@ exports.logout = async ctx => {
ctx.body = { message: "User logged out" } 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) => { 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, { return passport.authenticate(strategy, {
scope: ["profile", "email"], scope: ["profile", "email"],
@ -44,7 +53,7 @@ exports.googlePreAuth = async (ctx, next) => {
} }
exports.googleAuth = async (ctx, next) => { exports.googleAuth = async (ctx, next) => {
const strategy = await google.strategyFactory() const strategy = await google.strategyFactory(ctx)
return passport.authenticate( return passport.authenticate(
strategy, strategy,

View File

@ -1,7 +1,6 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../../controllers/admin/configs") const controller = require("../../controllers/admin/configs")
const joiValidator = require("../../../middleware/joi-validator") const joiValidator = require("../../../middleware/joi-validator")
const { authenticated } = require("@budibase/auth")
const Joi = require("joi") const Joi = require("joi")
const { Configs } = require("../../../constants") const { Configs } = require("../../../constants")
@ -16,9 +15,8 @@ function buildConfigSaveValidation() {
router router
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save) .post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
.post("/api/admin/configs/status", controller.configStatus)
.delete("/api/admin/configs/:id", controller.destroy) .delete("/api/admin/configs/:id", controller.destroy)
.get("/api/admin/configs", controller.fetch) .get("/api/admin/configs", controller.fetch)
.get("/api/admin/configs/:id", controller.find) .get("/api/admin/configs/:type", controller.find)
module.exports = router module.exports = router