From 1956d9765ec2eea407536ab85aa905b872b95777 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 19 May 2021 13:17:50 +0100 Subject: [PATCH] Adding admin only endpoint, removing the ability to create/delete global users from the app server and adding a global self user update. --- packages/server/src/api/controllers/row.js | 5 +- packages/server/src/api/controllers/user.js | 55 ++---------- packages/server/src/api/routes/user.js | 6 -- packages/server/src/automations/actions.js | 3 - .../src/automations/steps/createUser.js | 90 ------------------- .../worker/src/api/controllers/admin/users.js | 16 ++++ .../worker/src/api/routes/admin/configs.js | 3 + packages/worker/src/api/routes/admin/users.js | 30 +++++-- packages/worker/src/middleware/adminOnly.js | 6 ++ 9 files changed, 59 insertions(+), 155 deletions(-) delete mode 100644 packages/server/src/automations/steps/createUser.js create mode 100644 packages/worker/src/middleware/adminOnly.js diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 1d73abea47..6cf53a06c7 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -167,11 +167,10 @@ exports.save = async function (ctx) { table, }) - // TODO remove special user case in future - if (row.tableId === InternalTables.USER_METADATA) { + if (row.tableId === InternalTables.USER_METADATA && row._id) { // the row has been updated, need to put it into the ctx ctx.request.body = row - await userController.createMetadata(ctx) + await userController.updateMetadata(ctx) return } diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 39901990c2..d549231320 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -2,15 +2,10 @@ const CouchDB = require("../../db") const { generateUserMetadataID, getUserMetadataParams, - getGlobalIDFromUserMetadataID, } = require("../../db/utils") const { InternalTables } = require("../../db/utils") -const { getRole, BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") -const { - getGlobalUsers, - saveGlobalUser, - deleteGlobalUser, -} = require("../../utilities/workerRequests") +const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") +const { getGlobalUsers } = require("../../utilities/workerRequests") const { getFullUser } = require("../../utilities/users") exports.fetchMetadata = async function (ctx) { @@ -38,38 +33,6 @@ exports.fetchMetadata = async function (ctx) { ctx.body = users } -exports.createMetadata = async function (ctx) { - const appId = ctx.appId - const db = new CouchDB(appId) - const { roleId } = ctx.request.body - - if (ctx.request.body._id) { - return exports.updateMetadata(ctx) - } - - // check role valid - const role = await getRole(appId, roleId) - if (!role) ctx.throw(400, "Invalid Role") - - const globalUser = await saveGlobalUser(ctx, appId, ctx.request.body) - - const user = { - ...globalUser, - _id: generateUserMetadataID(globalUser._id), - type: "user", - tableId: InternalTables.USER_METADATA, - } - - const response = await db.post(user) - // for automations to make it obvious was successful - ctx.status = 200 - ctx.body = { - _id: response.id, - _rev: response.rev, - email: ctx.request.body.email, - } -} - exports.updateSelfMetadata = async function (ctx) { // overwrite the ID with current users ctx.request.body._id = ctx.user._id @@ -85,14 +48,13 @@ exports.updateMetadata = async function (ctx) { const appId = ctx.appId const db = new CouchDB(appId) const user = ctx.request.body - const globalUser = await saveGlobalUser(ctx, appId, { - ...user, - _id: getGlobalIDFromUserMetadataID(user._id), - }) + // make sure to always remove global user props + delete user.password + delete user.roles + delete user.builder const metadata = { - ...globalUser, tableId: InternalTables.USER_METADATA, - _id: user._id || generateUserMetadataID(globalUser._id), + _id: user._id, _rev: user._rev, } ctx.body = await db.put(metadata) @@ -100,7 +62,6 @@ exports.updateMetadata = async function (ctx) { exports.destroyMetadata = async function (ctx) { const db = new CouchDB(ctx.appId) - await deleteGlobalUser(ctx, getGlobalIDFromUserMetadataID(ctx.params.id)) try { const dbUser = await db.get(ctx.params.id) await db.remove(dbUser._id, dbUser._rev) @@ -108,7 +69,7 @@ exports.destroyMetadata = async function (ctx) { // error just means the global user has no config in this app } ctx.body = { - message: `User ${ctx.params.id} deleted.`, + message: `User metadata ${ctx.params.id} deleted.`, } } diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 01af18b933..b3b486fe45 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -25,12 +25,6 @@ router authorized(PermissionTypes.USER, PermissionLevels.WRITE), controller.updateMetadata ) - .post( - "/api/users/metadata", - authorized(PermissionTypes.USER, PermissionLevels.WRITE), - usage, - controller.createMetadata - ) .post( "/api/users/metadata/self", authorized(PermissionTypes.USER, PermissionLevels.WRITE), diff --git a/packages/server/src/automations/actions.js b/packages/server/src/automations/actions.js index 983e87854a..ad102e7b67 100644 --- a/packages/server/src/automations/actions.js +++ b/packages/server/src/automations/actions.js @@ -3,7 +3,6 @@ const sendSmtpEmail = require("./steps/sendSmtpEmail") const createRow = require("./steps/createRow") const updateRow = require("./steps/updateRow") const deleteRow = require("./steps/deleteRow") -const createUser = require("./steps/createUser") const executeScript = require("./steps/executeScript") const executeQuery = require("./steps/executeQuery") const outgoingWebhook = require("./steps/outgoingWebhook") @@ -20,7 +19,6 @@ const BUILTIN_ACTIONS = { CREATE_ROW: createRow.run, UPDATE_ROW: updateRow.run, DELETE_ROW: deleteRow.run, - CREATE_USER: createUser.run, OUTGOING_WEBHOOK: outgoingWebhook.run, EXECUTE_SCRIPT: executeScript.run, EXECUTE_QUERY: executeQuery.run, @@ -31,7 +29,6 @@ const BUILTIN_DEFINITIONS = { CREATE_ROW: createRow.definition, UPDATE_ROW: updateRow.definition, DELETE_ROW: deleteRow.definition, - CREATE_USER: createUser.definition, OUTGOING_WEBHOOK: outgoingWebhook.definition, EXECUTE_SCRIPT: executeScript.definition, EXECUTE_QUERY: executeQuery.definition, diff --git a/packages/server/src/automations/steps/createUser.js b/packages/server/src/automations/steps/createUser.js deleted file mode 100644 index 02250901fc..0000000000 --- a/packages/server/src/automations/steps/createUser.js +++ /dev/null @@ -1,90 +0,0 @@ -const roles = require("@budibase/auth/roles") -const userController = require("../../api/controllers/user") -const env = require("../../environment") -const usage = require("../../utilities/usageQuota") - -module.exports.definition = { - description: "Create a new user", - tagline: "Create user {{inputs.email}}", - icon: "ri-user-add-line", - name: "Create User", - type: "ACTION", - stepId: "CREATE_USER", - inputs: { - roleId: roles.BUILTIN_ROLE_IDS.POWER, - }, - schema: { - inputs: { - properties: { - email: { - type: "string", - customType: "email", - title: "Email", - }, - password: { - type: "string", - title: "Password", - }, - roleId: { - type: "string", - title: "Role", - enum: roles.BUILTIN_ROLE_ID_ARRAY, - pretty: roles.BUILTIN_ROLE_NAME_ARRAY, - }, - }, - required: ["email", "password", "roleId"], - }, - outputs: { - properties: { - id: { - type: "string", - description: "The identifier of the new user", - }, - revision: { - type: "string", - description: "The revision of the new user", - }, - response: { - type: "object", - description: "The response from the user table", - }, - success: { - type: "boolean", - description: "Whether the action was successful", - }, - }, - required: ["id", "revision", "success"], - }, - }, -} - -module.exports.run = async function ({ inputs, appId, apiKey, emitter }) { - const { email, password, roleId } = inputs - const ctx = { - appId, - request: { - body: { email, password, roleId }, - }, - eventEmitter: emitter, - } - - try { - if (env.isProd()) { - await usage.update(apiKey, usage.Properties.USER, 1) - } - await userController.createMetadata(ctx) - return { - response: ctx.body, - // internal property not returned through the API - id: ctx.body._id, - revision: ctx.body._rev, - success: ctx.status === 200, - } - } catch (err) { - console.error(err) - return { - success: false, - response: err, - } - } -} diff --git a/packages/worker/src/api/controllers/admin/users.js b/packages/worker/src/api/controllers/admin/users.js index ce452d5c78..68198223a2 100644 --- a/packages/worker/src/api/controllers/admin/users.js +++ b/packages/worker/src/api/controllers/admin/users.js @@ -96,6 +96,22 @@ exports.destroy = async ctx => { } } +exports.self = async ctx => { + const db = new CouchDB(GLOBAL_DB) + const user = await db.get(ctx.user._id) + if (ctx.request.body.password) { + ctx.request.body.password = await hash(ctx.request.body.password) + } + const response = await db.put({ + ...user, + ...ctx.request.body, + }) + ctx.body = { + _id: response.id, + _rev: response.rev, + } +} + // called internally by app server user fetch exports.fetch = async ctx => { const db = new CouchDB(GLOBAL_DB) diff --git a/packages/worker/src/api/routes/admin/configs.js b/packages/worker/src/api/routes/admin/configs.js index a028ebdd81..a6fd9f5dd5 100644 --- a/packages/worker/src/api/routes/admin/configs.js +++ b/packages/worker/src/api/routes/admin/configs.js @@ -1,6 +1,7 @@ const Router = require("@koa/router") const controller = require("../../controllers/admin/configs") const joiValidator = require("../../../middleware/joi-validator") +const adminOnly = require("../../../middleware/adminOnly") const Joi = require("joi") const { Configs, ConfigUploads } = require("../../../constants") @@ -76,6 +77,8 @@ function buildConfigGetValidation() { }).unknown(true).required()) } +router.use(adminOnly) + router .post("/api/admin/configs", buildConfigSaveValidation(), controller.save) .delete("/api/admin/configs/:id", controller.destroy) diff --git a/packages/worker/src/api/routes/admin/users.js b/packages/worker/src/api/routes/admin/users.js index cac1d5af9c..1f6aebb191 100644 --- a/packages/worker/src/api/routes/admin/users.js +++ b/packages/worker/src/api/routes/admin/users.js @@ -5,13 +5,12 @@ const Joi = require("joi") const router = Router() -function buildUserSaveValidation() { - // prettier-ignore - return joiValidator.body(Joi.object({ - _id: Joi.string(), - _rev: Joi.string(), +function buildUserSaveValidation(isSelf = false) { + let schema = { email: Joi.string(), password: Joi.string().allow(null, ""), + firstName: Joi.string(), + lastName: Joi.string(), builder: Joi.object({ global: Joi.boolean().optional(), apps: Joi.array().optional(), @@ -21,7 +20,17 @@ function buildUserSaveValidation() { .pattern(/.*/, Joi.string()) .required() .unknown(true) - }).required().unknown(true)) + } + if (!isSelf) { + schema = { + ...schema, + _id: Joi.string(), + _rev: Joi.string(), + } + } + return joiValidator.body(Joi.object(schema) + .required() + .unknown(true)) } function buildInviteValidation() { @@ -39,10 +48,19 @@ function buildInviteAcceptValidation() { }).required().unknown(true)) } +function buildUpdateSelfValidation() { + // prettier-ignore + return joiValidator.body(Joi.object({ + inviteCode: Joi.string().required(), + password: Joi.string().required(), + }).required().unknown(true)) +} + router .post("/api/admin/users", buildUserSaveValidation(), controller.save) .get("/api/admin/users", controller.fetch) .post("/api/admin/users/init", controller.adminUser) + .post("/api/admin/users/self", buildUserSaveValidation(true), controller.self) .delete("/api/admin/users/:id", controller.destroy) .get("/api/admin/users/:id", controller.find) .get("/api/admin/roles/:appId") diff --git a/packages/worker/src/middleware/adminOnly.js b/packages/worker/src/middleware/adminOnly.js new file mode 100644 index 0000000000..0f1a77346d --- /dev/null +++ b/packages/worker/src/middleware/adminOnly.js @@ -0,0 +1,6 @@ +module.exports = async (ctx, next) => { + if (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) { + ctx.throw(403, "Admin user only endpoint.") + } + return next() +} \ No newline at end of file