From 34a12831a62ef7c2079655281ed178b760d13d73 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Mon, 19 Apr 2021 11:34:07 +0100 Subject: [PATCH 1/3] basic group apis --- packages/auth/package.json | 3 +- packages/auth/src/constants.js | 7 ++ packages/auth/src/db/utils.js | 11 +++ packages/auth/src/hashing.js | 5 + packages/auth/src/index.js | 2 + packages/auth/yarn.lock | 5 + .../src/api/controllers/admin/groups.js | 26 ++++++ .../worker/src/api/controllers/admin/index.js | 93 ++----------------- .../worker/src/api/controllers/admin/users.js | 90 ++++++++++++++++++ .../worker/src/api/routes/admin/groups.js | 39 ++++++++ packages/worker/src/api/routes/admin/index.js | 42 +-------- packages/worker/src/api/routes/admin/users.js | 39 ++++++++ packages/worker/src/api/routes/index.js | 4 +- packages/worker/src/constants/index.js | 4 + 14 files changed, 244 insertions(+), 126 deletions(-) create mode 100644 packages/worker/src/api/controllers/admin/groups.js create mode 100644 packages/worker/src/api/controllers/admin/users.js create mode 100644 packages/worker/src/api/routes/admin/groups.js create mode 100644 packages/worker/src/api/routes/admin/users.js diff --git a/packages/auth/package.json b/packages/auth/package.json index 294b09321d..b4f4b1cb33 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -12,6 +12,7 @@ "passport-google-auth": "^1.0.2", "passport-google-oauth": "^2.0.0", "passport-jwt": "^4.0.0", - "passport-local": "^1.0.0" + "passport-local": "^1.0.0", + "uuid": "^8.3.2" } } diff --git a/packages/auth/src/constants.js b/packages/auth/src/constants.js index b6946a543b..8ca05066c9 100644 --- a/packages/auth/src/constants.js +++ b/packages/auth/src/constants.js @@ -7,3 +7,10 @@ exports.Cookies = { CurrentApp: "budibase:currentapp", Auth: "budibase:auth", } + +exports.GlobalRoles = { + OWNER: "owner", + ADMIN: "admin", + BUILDER: "builder", + GROUP_MANAGER: "group_manager", +} diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 043987fcc4..855c0da265 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -1,3 +1,5 @@ +const { newid } = require("../hashing") + exports.StaticDatabases = { USER: { name: "user-db", @@ -7,6 +9,7 @@ exports.StaticDatabases = { const DocumentTypes = { USER: "us", APP: "app", + GROUP: "group", } exports.DocumentTypes = DocumentTypes @@ -29,6 +32,14 @@ exports.getEmailFromUserID = userId => { return userId.split(`${DocumentTypes.USER}${SEPARATOR}`)[1] } +/** + * Generates a new group ID. + * @returns {string} The new group ID which the group doc can be stored under. + */ +exports.generateGroupID = () => { + return `${DocumentTypes.GROUP}${SEPARATOR}${newid()}` +} + /** * Gets parameters for retrieving users, this is a utility function for the getDocParams function. */ diff --git a/packages/auth/src/hashing.js b/packages/auth/src/hashing.js index 0711ae67bf..65976fc1f3 100644 --- a/packages/auth/src/hashing.js +++ b/packages/auth/src/hashing.js @@ -1,5 +1,6 @@ const bcrypt = require("bcryptjs") const env = require("./environment") +const { v4 } = require("uuid") const SALT_ROUNDS = env.SALT_ROUNDS || 10 @@ -11,3 +12,7 @@ exports.hash = async data => { exports.compare = async (data, encrypted) => { return bcrypt.compare(data, encrypted) } + +exports.newid = function() { + return v4().replace(/-/g, "") +} diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 705e07f813..38c56a5ddd 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -17,6 +17,8 @@ const { const { generateUserID, getUserParams, + generateGroupID, + getGroupParams, getEmailFromUserID, } = require("./db/utils") diff --git a/packages/auth/yarn.lock b/packages/auth/yarn.lock index 6771306258..c3066ebdc1 100644 --- a/packages/auth/yarn.lock +++ b/packages/auth/yarn.lock @@ -584,6 +584,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" diff --git a/packages/worker/src/api/controllers/admin/groups.js b/packages/worker/src/api/controllers/admin/groups.js new file mode 100644 index 0000000000..58ea14bc5c --- /dev/null +++ b/packages/worker/src/api/controllers/admin/groups.js @@ -0,0 +1,26 @@ +const CouchDB = require("../../../db") +const { + hash, + generateUserID, + getUserParams, + StaticDatabases, +} = require("@budibase/auth") +const { UserStatus } = require("../../../constants") + +const USER_DB = StaticDatabases.USER.name + +exports.save = async function(ctx, next) { + const db = new CouchDB(USER_DB) +} + +exports.fetch = async function(ctx, next) { + const db = new CouchDB(USER_DB) +} + +exports.find = async function(ctx, next) { + const db = new CouchDB(USER_DB) +} + +exports.destroy = async function(ctx, next) { + const db = new CouchDB(USER_DB) +} diff --git a/packages/worker/src/api/controllers/admin/index.js b/packages/worker/src/api/controllers/admin/index.js index ff0d2997e7..63934ed6c5 100644 --- a/packages/worker/src/api/controllers/admin/index.js +++ b/packages/worker/src/api/controllers/admin/index.js @@ -1,90 +1,11 @@ -const CouchDB = require("../../../db") -const { - hash, - generateUserID, - getUserParams, - StaticDatabases, -} = require("@budibase/auth") -const { UserStatus } = require("../../../constants") +const users = require("./users") +const groups = require("./groups") -const USER_DB = StaticDatabases.USER.name - -exports.userSave = async ctx => { - const db = new CouchDB(USER_DB) - const { email, password, _id } = ctx.request.body - const hashedPassword = password ? await hash(password) : null - let user = { - ...ctx.request.body, - _id: generateUserID(email), - password: hashedPassword, - } - let dbUser - // in-case user existed already - if (_id) { - dbUser = await db.get(_id) - } - // add the active status to a user if its not provided - if (user.status == null) { - user.status = UserStatus.ACTIVE - } - try { - const response = await db.post({ - password: hashedPassword || dbUser.password, - ...user, - }) - ctx.body = { - _id: response.id, - _rev: response.rev, - email, - } - } catch (err) { - if (err.status === 409) { - ctx.throw(400, "User exists already") - } else { - ctx.throw(err.status, err) - } - } +exports.initialise = async function(ctx) { + // create the ALL users group } -exports.userDelete = async ctx => { - const db = new CouchDB(USER_DB) - const dbUser = await db.get(generateUserID(ctx.params.email)) - await db.remove(dbUser._id, dbUser._rev) - ctx.body = { - message: `User ${ctx.params.email} deleted.`, - } -} - -// called internally by app server user fetch -exports.userFetch = async ctx => { - const db = new CouchDB(USER_DB) - const response = await db.allDocs( - getUserParams(null, { - include_docs: true, - }) - ) - const users = response.rows.map(row => row.doc) - // user hashed password shouldn't ever be returned - for (let user of users) { - if (user) { - delete user.password - } - } - ctx.body = users -} - -// called internally by app server user find -exports.userFind = async ctx => { - const db = new CouchDB(USER_DB) - let user - try { - user = await db.get(generateUserID(ctx.params.email)) - } catch (err) { - // no user found, just return nothing - user = {} - } - if (user) { - delete user.password - } - ctx.body = user +module.exports = { + users, + groups, } diff --git a/packages/worker/src/api/controllers/admin/users.js b/packages/worker/src/api/controllers/admin/users.js new file mode 100644 index 0000000000..ff0d2997e7 --- /dev/null +++ b/packages/worker/src/api/controllers/admin/users.js @@ -0,0 +1,90 @@ +const CouchDB = require("../../../db") +const { + hash, + generateUserID, + getUserParams, + StaticDatabases, +} = require("@budibase/auth") +const { UserStatus } = require("../../../constants") + +const USER_DB = StaticDatabases.USER.name + +exports.userSave = async ctx => { + const db = new CouchDB(USER_DB) + const { email, password, _id } = ctx.request.body + const hashedPassword = password ? await hash(password) : null + let user = { + ...ctx.request.body, + _id: generateUserID(email), + password: hashedPassword, + } + let dbUser + // in-case user existed already + if (_id) { + dbUser = await db.get(_id) + } + // add the active status to a user if its not provided + if (user.status == null) { + user.status = UserStatus.ACTIVE + } + try { + const response = await db.post({ + password: hashedPassword || dbUser.password, + ...user, + }) + ctx.body = { + _id: response.id, + _rev: response.rev, + email, + } + } catch (err) { + if (err.status === 409) { + ctx.throw(400, "User exists already") + } else { + ctx.throw(err.status, err) + } + } +} + +exports.userDelete = async ctx => { + const db = new CouchDB(USER_DB) + const dbUser = await db.get(generateUserID(ctx.params.email)) + await db.remove(dbUser._id, dbUser._rev) + ctx.body = { + message: `User ${ctx.params.email} deleted.`, + } +} + +// called internally by app server user fetch +exports.userFetch = async ctx => { + const db = new CouchDB(USER_DB) + const response = await db.allDocs( + getUserParams(null, { + include_docs: true, + }) + ) + const users = response.rows.map(row => row.doc) + // user hashed password shouldn't ever be returned + for (let user of users) { + if (user) { + delete user.password + } + } + ctx.body = users +} + +// called internally by app server user find +exports.userFind = async ctx => { + const db = new CouchDB(USER_DB) + let user + try { + user = await db.get(generateUserID(ctx.params.email)) + } catch (err) { + // no user found, just return nothing + user = {} + } + if (user) { + delete user.password + } + ctx.body = user +} diff --git a/packages/worker/src/api/routes/admin/groups.js b/packages/worker/src/api/routes/admin/groups.js new file mode 100644 index 0000000000..ca8c675dc7 --- /dev/null +++ b/packages/worker/src/api/routes/admin/groups.js @@ -0,0 +1,39 @@ +const Router = require("@koa/router") +const controller = require("../../controllers/admin/groups") +const joiValidator = require("../../../middleware/joi-validator") +const { authenticated } = require("@budibase/auth") +const Joi = require("joi") + +const router = Router() + +function buildGroupSaveValidation() { + // prettier-ignore + return joiValidator.body(Joi.object({ + // _id: Joi.string(), + // _rev: Joi.string(), + // email: Joi.string(), + // password: Joi.string().allow(null, ""), + // builder: Joi.object({ + // global: Joi.boolean().optional(), + // apps: Joi.array().optional(), + // }).unknown(true).optional(), + // // maps appId -> roleId for the user + // roles: Joi.object() + // .pattern(/.*/, Joi.string()) + // .required() + // .unknown(true) + }).required().unknown(true).optional()) +} + +router + .post( + "/api/admin/groups", + buildGroupSaveValidation(), + authenticated, + controller.save + ) + .delete("/api/admin/groups/:id", authenticated, controller.destroy) + .get("/api/admin/groups", authenticated, controller.fetch) + .get("/api/admin/group/:id", authenticated, controller.find) + +module.exports = router diff --git a/packages/worker/src/api/routes/admin/index.js b/packages/worker/src/api/routes/admin/index.js index 77c46278d1..ce52a85d05 100644 --- a/packages/worker/src/api/routes/admin/index.js +++ b/packages/worker/src/api/routes/admin/index.js @@ -1,39 +1,7 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/admin") -const joiValidator = require("../../../middleware/joi-validator") -const { authenticated } = require("@budibase/auth") -const Joi = require("joi") +const groups = require("./groups") +const users = require("./users") -const router = Router() - -function buildUserSaveValidation() { - // prettier-ignore - return joiValidator.body(Joi.object({ - _id: Joi.string(), - _rev: Joi.string(), - email: Joi.string(), - password: Joi.string().allow(null, ""), - builder: Joi.object({ - global: Joi.boolean().optional(), - apps: Joi.array().optional(), - }).unknown(true).optional(), - // maps appId -> roleId for the user - roles: Joi.object() - .pattern(/.*/, Joi.string()) - .required() - .unknown(true) - }).required().unknown(true).optional()) +module.exports = { + groups, + users, } - -router - .post( - "/api/admin/users", - buildUserSaveValidation(), - authenticated, - controller.userSave - ) - .delete("/api/admin/users/:email", authenticated, controller.userDelete) - .get("/api/admin/users", authenticated, controller.userFetch) - .get("/api/admin/users/:email", authenticated, controller.userFind) - -module.exports = router diff --git a/packages/worker/src/api/routes/admin/users.js b/packages/worker/src/api/routes/admin/users.js new file mode 100644 index 0000000000..fe8e57593a --- /dev/null +++ b/packages/worker/src/api/routes/admin/users.js @@ -0,0 +1,39 @@ +const Router = require("@koa/router") +const controller = require("../../controllers/admin/users") +const joiValidator = require("../../../middleware/joi-validator") +const { authenticated } = require("@budibase/auth") +const Joi = require("joi") + +const router = Router() + +function buildUserSaveValidation() { + // prettier-ignore + return joiValidator.body(Joi.object({ + _id: Joi.string(), + _rev: Joi.string(), + email: Joi.string(), + password: Joi.string().allow(null, ""), + builder: Joi.object({ + global: Joi.boolean().optional(), + apps: Joi.array().optional(), + }).unknown(true).optional(), + // maps appId -> roleId for the user + roles: Joi.object() + .pattern(/.*/, Joi.string()) + .required() + .unknown(true) + }).required().unknown(true).optional()) +} + +router + .post( + "/api/admin/users", + buildUserSaveValidation(), + authenticated, + controller.userSave + ) + .delete("/api/admin/users/:email", authenticated, controller.userDelete) + .get("/api/admin/users", authenticated, controller.userFetch) + .get("/api/admin/users/:email", authenticated, controller.userFind) + +module.exports = router diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index c6bacc81e5..d4a8cfc073 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -1,5 +1,5 @@ -const adminRoutes = require("./admin") +const { userRoutes, groupRoutes } = require("./admin") const authRoutes = require("./auth") const appRoutes = require("./app") -exports.routes = [adminRoutes, authRoutes, appRoutes] +exports.routes = [userRoutes, groupRoutes, authRoutes, appRoutes] diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js index 77a6aedca0..586d69c86f 100644 --- a/packages/worker/src/constants/index.js +++ b/packages/worker/src/constants/index.js @@ -2,3 +2,7 @@ exports.UserStatus = { ACTIVE: "active", INACTIVE: "inactive", } + +exports.Groups = { + ALL_USERS: "all_users", +} From 4b5d302819421505723d6ec91dbe63d417ee5d28 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Mon, 19 Apr 2021 16:16:46 +0100 Subject: [PATCH 2/3] group CRUD --- packages/auth/src/db/utils.js | 13 +++- packages/auth/src/index.js | 2 + .../src/api/controllers/admin/groups.js | 70 ++++++++++++------- .../worker/src/api/routes/admin/groups.js | 29 ++++---- packages/worker/src/api/routes/admin/index.js | 7 -- packages/worker/src/api/routes/index.js | 3 +- 6 files changed, 76 insertions(+), 48 deletions(-) delete mode 100644 packages/worker/src/api/routes/admin/index.js diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 56e1ea2154..89aedb60bf 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -41,7 +41,18 @@ exports.generateGroupID = () => { } /** - * Gets parameters for retrieving users, this is a utility function for the getDocParams function. + * Gets parameters for retrieving groups. + */ +exports.getGroupParams = (id = "", otherProps = {}) => { + return { + ...otherProps, + startkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}`, + endkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}${UNICODE_MAX}`, + } +} + +/** + * Gets parameters for retrieving users. */ exports.getUserParams = (email = "", otherProps = {}) => { if (!email) { diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 85b60e4f25..455181d538 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -51,6 +51,8 @@ module.exports = { StaticDatabases, generateUserID, getUserParams, + generateGroupID, + getGroupParams, getEmailFromUserID, hash, compare, diff --git a/packages/worker/src/api/controllers/admin/groups.js b/packages/worker/src/api/controllers/admin/groups.js index 31a1b66e8e..3642c2464d 100644 --- a/packages/worker/src/api/controllers/admin/groups.js +++ b/packages/worker/src/api/controllers/admin/groups.js @@ -1,36 +1,58 @@ const CouchDB = require("../../../db") -const { - hash, - generateUserID, - getUserParams, - StaticDatabases, -} = require("@budibase/auth") -const { UserStatus } = require("../../../constants") +const { getGroupParams, StaticDatabases } = require("@budibase/auth") +const { generateGroupID } = require("@budibase/auth") -const USER_DB = StaticDatabases.USER.name +const GLOBAL_DB = StaticDatabases.GLOBAL.name -exports.save = async function(ctx, next) { - const db = new CouchDB(USER_DB) - const doc = ctx.request.body +exports.save = async function(ctx) { + const db = new CouchDB(GLOBAL_DB) + const groupDoc = ctx.request.body - const groupDoc = { - users: ["us:1234", "us:1234"], - managers: ["us:1234"], - defaultRole: "BASIC", - apps: { - abc123: "ADMIN", - }, + // Group does not exist yet + if (!groupDoc._id) { + groupDoc._id = generateGroupID() + } + + try { + const response = await db.post(groupDoc) + ctx.body = { + _id: response.id, + _rev: response.rev, + } + } catch (err) { + ctx.throw(err.status, err) } } -exports.fetch = async function(ctx, next) { - const db = new CouchDB(USER_DB) +exports.fetch = async function(ctx) { + const db = new CouchDB(GLOBAL_DB) + const response = await db.allDocs( + getGroupParams(undefined, { + include_docs: true, + }) + ) + const groups = response.rows.map(row => row.doc) + ctx.body = groups } -exports.find = async function(ctx, next) { - const db = new CouchDB(USER_DB) +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, next) { - const db = new CouchDB(USER_DB) +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: "Group deleted successfully" } + } catch (err) { + ctx.throw(err.status, err) + } } diff --git a/packages/worker/src/api/routes/admin/groups.js b/packages/worker/src/api/routes/admin/groups.js index ca8c675dc7..e683a01c2b 100644 --- a/packages/worker/src/api/routes/admin/groups.js +++ b/packages/worker/src/api/routes/admin/groups.js @@ -9,20 +9,19 @@ const router = Router() function buildGroupSaveValidation() { // prettier-ignore return joiValidator.body(Joi.object({ - // _id: Joi.string(), - // _rev: Joi.string(), - // email: Joi.string(), - // password: Joi.string().allow(null, ""), - // builder: Joi.object({ - // global: Joi.boolean().optional(), - // apps: Joi.array().optional(), - // }).unknown(true).optional(), - // // maps appId -> roleId for the user - // roles: Joi.object() - // .pattern(/.*/, Joi.string()) - // .required() - // .unknown(true) - }).required().unknown(true).optional()) + _id: Joi.string().optional(), + _rev: Joi.string().optional(), + name: Joi.string().required(), + users: Joi.array().required(), + managers: Joi.array().required(), + roles: Joi.object({ + default: Joi.string().optional(), + app: Joi.object() + .pattern(/.*/, Joi.string()) + .required() + .unknown(true), + }).unknown(true).optional(), + }).required().unknown(true)) } router @@ -34,6 +33,6 @@ router ) .delete("/api/admin/groups/:id", authenticated, controller.destroy) .get("/api/admin/groups", authenticated, controller.fetch) - .get("/api/admin/group/:id", authenticated, controller.find) + .get("/api/admin/groups/:id", authenticated, controller.find) module.exports = router diff --git a/packages/worker/src/api/routes/admin/index.js b/packages/worker/src/api/routes/admin/index.js deleted file mode 100644 index ce52a85d05..0000000000 --- a/packages/worker/src/api/routes/admin/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const groups = require("./groups") -const users = require("./users") - -module.exports = { - groups, - users, -} diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index d4a8cfc073..5c6b088443 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -1,4 +1,5 @@ -const { userRoutes, groupRoutes } = require("./admin") +const userRoutes = require("./admin/users") +const groupRoutes = require("./admin/groups") const authRoutes = require("./auth") const appRoutes = require("./app") From b5e19e4216703d0c799e5790c7898a72581833fd Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 20 Apr 2021 13:13:15 +0100 Subject: [PATCH 3/3] lint --- packages/worker/src/api/controllers/admin/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/worker/src/api/controllers/admin/index.js b/packages/worker/src/api/controllers/admin/index.js index 63934ed6c5..60ca3b2900 100644 --- a/packages/worker/src/api/controllers/admin/index.js +++ b/packages/worker/src/api/controllers/admin/index.js @@ -1,10 +1,6 @@ const users = require("./users") const groups = require("./groups") -exports.initialise = async function(ctx) { - // create the ALL users group -} - module.exports = { users, groups,