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 a4c0cce0dd..49c1c90159 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -1,4 +1,4 @@ -const uuid = require("uuid/v4") +const { newid } = require("../hashing") exports.ViewNames = { USER_BY_EMAIL: "by_email", @@ -13,6 +13,7 @@ exports.StaticDatabases = { const DocumentTypes = { USER: "us", APP: "app", + GROUP: "group", } exports.DocumentTypes = DocumentTypes @@ -27,11 +28,30 @@ exports.SEPARATOR = SEPARATOR * @returns {string} The new user ID which the user doc can be stored under. */ exports.generateUserID = () => { - return `${DocumentTypes.USER}${SEPARATOR}${uuid()}` + return `${DocumentTypes.USER}${SEPARATOR}${newid()}` } /** - * Gets parameters for retrieving users, this is a utility function for the getDocParams function. + * 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 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 = (globalId = "", otherProps = {}) => { if (!globalId) { 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 2dbd027061..9ee3a11bed 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -17,6 +17,8 @@ const { const { generateUserID, getUserParams, + generateGroupID, + getGroupParams, } = require("./db/utils") const { getGlobalUserByEmail } = require("./utils") @@ -49,6 +51,8 @@ module.exports = { StaticDatabases, generateUserID, getUserParams, + generateGroupID, + getGroupParams, hash, compare, getAppId, 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..3642c2464d --- /dev/null +++ b/packages/worker/src/api/controllers/admin/groups.js @@ -0,0 +1,58 @@ +const CouchDB = require("../../../db") +const { getGroupParams, StaticDatabases } = require("@budibase/auth") +const { generateGroupID } = require("@budibase/auth") + +const GLOBAL_DB = StaticDatabases.GLOBAL.name + +exports.save = async function(ctx) { + const db = new CouchDB(GLOBAL_DB) + const groupDoc = ctx.request.body + + // 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) { + 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) { + 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: "Group deleted successfully" } + } catch (err) { + ctx.throw(err.status, err) + } +} diff --git a/packages/worker/src/api/controllers/admin/index.js b/packages/worker/src/api/controllers/admin/index.js index 600a8e75f6..60ca3b2900 100644 --- a/packages/worker/src/api/controllers/admin/index.js +++ b/packages/worker/src/api/controllers/admin/index.js @@ -1,90 +1,7 @@ -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 GLOBAL_DB = StaticDatabases.GLOBAL.name - -exports.userSave = async ctx => { - const db = new CouchDB(GLOBAL_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(GLOBAL_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(GLOBAL_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(GLOBAL_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..600a8e75f6 --- /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 GLOBAL_DB = StaticDatabases.GLOBAL.name + +exports.userSave = async ctx => { + const db = new CouchDB(GLOBAL_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(GLOBAL_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(GLOBAL_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(GLOBAL_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..e683a01c2b --- /dev/null +++ b/packages/worker/src/api/routes/admin/groups.js @@ -0,0 +1,38 @@ +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().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 + .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/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/users.js similarity index 94% rename from packages/worker/src/api/routes/admin/index.js rename to packages/worker/src/api/routes/admin/users.js index 77c46278d1..fe8e57593a 100644 --- a/packages/worker/src/api/routes/admin/index.js +++ b/packages/worker/src/api/routes/admin/users.js @@ -1,5 +1,5 @@ const Router = require("@koa/router") -const controller = require("../../controllers/admin") +const controller = require("../../controllers/admin/users") const joiValidator = require("../../../middleware/joi-validator") const { authenticated } = require("@budibase/auth") const Joi = require("joi") diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index c6bacc81e5..5c6b088443 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -1,5 +1,6 @@ -const adminRoutes = require("./admin") +const userRoutes = require("./admin/users") +const groupRoutes = require("./admin/groups") 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", +}