From b4c8bf81f7a380639dc3259e1441c0e40242dbdc Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 20 Apr 2021 17:17:44 +0100 Subject: [PATCH] Swapping over everything to use the new user ID and updating everything after some end to end testing. --- packages/auth/src/db/index.js | 8 ++- packages/auth/src/db/utils.js | 18 +++--- packages/auth/src/db/views.js | 21 ++++++- packages/auth/src/index.js | 16 ++--- .../auth/src/middleware/passport/local.js | 9 +-- packages/auth/src/utils.js | 10 +++- .../src/components/login/LoginForm.svelte | 9 +-- .../components/start/CreateAppModal.svelte | 4 +- packages/builder/src/stores/backend/auth.js | 13 ++++ packages/server/src/api/controllers/user.js | 24 +++++--- packages/server/src/api/routes/user.js | 6 ++ packages/server/src/middleware/currentapp.js | 33 +++++++---- .../server/src/utilities/workerRequests.js | 6 +- .../src/api/controllers/admin/groups.js | 6 +- .../worker/src/api/controllers/admin/users.js | 59 ++++++++++++++----- packages/worker/src/api/routes/admin/users.js | 5 +- packages/worker/src/db/utils.js | 35 ----------- 17 files changed, 163 insertions(+), 119 deletions(-) delete mode 100644 packages/worker/src/db/utils.js diff --git a/packages/auth/src/db/index.js b/packages/auth/src/db/index.js index 9ae48e68b1..f94fe4afea 100644 --- a/packages/auth/src/db/index.js +++ b/packages/auth/src/db/index.js @@ -1,5 +1,9 @@ +let Pouch + module.exports.setDB = pouch => { - module.exports.CouchDB = pouch + Pouch = pouch } -module.exports.CouchDB = null +module.exports.getDB = dbName => { + return new Pouch(dbName) +} diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 49c1c90159..2e2c3f1aed 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -23,14 +23,6 @@ const SEPARATOR = "_" exports.SEPARATOR = SEPARATOR -/** - * Generates a new global user ID. - * @returns {string} The new user ID which the user doc can be stored under. - */ -exports.generateUserID = () => { - return `${DocumentTypes.USER}${SEPARATOR}${newid()}` -} - /** * Generates a new group ID. * @returns {string} The new group ID which the group doc can be stored under. @@ -50,10 +42,18 @@ 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()}` +} + /** * Gets parameters for retrieving users. */ -exports.getUserParams = (globalId = "", otherProps = {}) => { +exports.getGlobalUserParams = (globalId = "", otherProps = {}) => { if (!globalId) { globalId = "" } diff --git a/packages/auth/src/db/views.js b/packages/auth/src/db/views.js index 86c919b012..1f1f28b917 100644 --- a/packages/auth/src/db/views.js +++ b/packages/auth/src/db/views.js @@ -1,9 +1,24 @@ const { DocumentTypes, ViewNames, StaticDatabases } = require("./utils") -const { CouchDB } = require("./index") +const { getDB } = require("./index") + +function DesignDoc() { + return { + _id: "_design/database", + // view collation information, read before writing any complex views: + // https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification + views: {}, + } +} exports.createUserEmailView = async () => { - const db = new CouchDB(StaticDatabases.GLOBAL.name) - const designDoc = await db.get("_design/database") + const db = getDB(StaticDatabases.GLOBAL.name) + let designDoc + try { + designDoc = await db.get("_design/database") + } catch (err) { + // no design doc, make one + designDoc = DesignDoc() + } const view = { // if using variables in a map function need to inject them before use map: `function(doc) { diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 9ee3a11bed..fee83b65d8 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -2,7 +2,7 @@ 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 database = require("./db") +const { setDB, getDB } = require("./db") const { StaticDatabases } = require("./db/utils") const { jwt, local, authenticated } = require("./middleware") const { Cookies, UserStatus } = require("./constants") @@ -13,14 +13,14 @@ const { getCookie, clearCookie, isClient, + getGlobalUserByEmail, } = require("./utils") const { - generateUserID, - getUserParams, + generateGlobalUserID, + getGlobalUserParams, generateGroupID, getGroupParams, } = require("./db/utils") -const { getGlobalUserByEmail } = require("./utils") // Strategies passport.use(new LocalStrategy(local.options, local.authenticate)) @@ -30,7 +30,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) passport.serializeUser((user, done) => done(null, user)) passport.deserializeUser(async (user, done) => { - const db = new database.CouchDB(StaticDatabases.GLOBAL.name) + const db = getDB(StaticDatabases.GLOBAL.name) try { const user = await db.get(user._id) @@ -43,14 +43,14 @@ passport.deserializeUser(async (user, done) => { module.exports = { init(pouch) { - database.setDB(pouch) + setDB(pouch) }, passport, Cookies, UserStatus, StaticDatabases, - generateUserID, - getUserParams, + generateGlobalUserID, + getGlobalUserParams, generateGroupID, getGroupParams, hash, diff --git a/packages/auth/src/middleware/passport/local.js b/packages/auth/src/middleware/passport/local.js index 333f381297..1942d0c424 100644 --- a/packages/auth/src/middleware/passport/local.js +++ b/packages/auth/src/middleware/passport/local.js @@ -1,7 +1,5 @@ const jwt = require("jsonwebtoken") const { UserStatus } = require("../../constants") -const database = require("../../db") -const { StaticDatabases, generateUserID } = require("../../db/utils") const { compare } = require("../../hashing") const env = require("../../environment") const { getGlobalUserByEmail } = require("../../utils") @@ -21,11 +19,8 @@ exports.authenticate = async function(email, password, done) { if (!email) return done(null, false, "Email Required.") if (!password) return done(null, false, "Password Required.") - let dbUser - try { - dbUser = await getGlobalUserByEmail(email) - } catch (err) { - console.error("User not found", err) + const dbUser = await getGlobalUserByEmail(email) + if (dbUser == null) { return done(null, false, { message: "User not found" }) } diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 2320896150..14925653c9 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -2,7 +2,7 @@ const { DocumentTypes, SEPARATOR, ViewNames, StaticDatabases } = require("./db/u const jwt = require("jsonwebtoken") const { options } = require("./middleware/passport/jwt") const { createUserEmailView } = require("./db/views") -const { CouchDB } = require("./db") +const { getDB } = require("./db") const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -101,19 +101,23 @@ exports.isClient = ctx => { } exports.getGlobalUserByEmail = async email => { - const db = new CouchDB(StaticDatabases.GLOBAL.name) + const db = getDB(StaticDatabases.GLOBAL.name) try { let users = (await db.query( `database/${ViewNames.USER_BY_EMAIL}`, { - key: email + key: email, + include_docs: true, }) ).rows + users = users.map(user => user.doc) return users.length <= 1 ? users[0] : users } catch (err) { if (err != null && err.name === "not_found") { await createUserEmailView() return exports.getGlobalUserByEmail(email) + } else { + throw err } } } diff --git a/packages/builder/src/components/login/LoginForm.svelte b/packages/builder/src/components/login/LoginForm.svelte index 7e32efb7c5..2222ebbdf7 100644 --- a/packages/builder/src/components/login/LoginForm.svelte +++ b/packages/builder/src/components/login/LoginForm.svelte @@ -21,14 +21,7 @@ async function createTestUser() { try { - await auth.createUser({ - email: "test@test.com", - password: "test", - roles: {}, - builder: { - global: true, - }, - }) + await auth.firstUser() notifier.success("Test user created") } catch (err) { console.error(err) diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index fcd5379733..3739b86b76 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -151,8 +151,8 @@ const user = { roleId: $createAppStore.values.roleId, } - const userResp = await api.post(`/api/users/metadata`, user) - const json = await userResp.json() + const userResp = await api.post(`/api/users/metadata/self`, user) + await userResp.json() $goto(`./${appJson._id}`) } catch (error) { console.error(error) diff --git a/packages/builder/src/stores/backend/auth.js b/packages/builder/src/stores/backend/auth.js index 0d59451417..29dd6dcdd0 100644 --- a/packages/builder/src/stores/backend/auth.js +++ b/packages/builder/src/stores/backend/auth.js @@ -30,11 +30,24 @@ export function createAuthStore() { }, logout: async () => { const response = await api.post(`/api/admin/auth/logout`) + if (response.status !== 200) { + throw "Unable to create logout" + } await response.json() set({ user: null }) }, createUser: async user => { const response = await api.post(`/api/admin/users`, user) + if (response.status !== 200) { + throw "Unable to create user" + } + await response.json() + }, + firstUser: async () => { + const response = await api.post(`/api/admin/users/first`) + if (response.status !== 200) { + throw "Unable to create test user" + } await response.json() }, } diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index eee1f3f048..4b6c65736a 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -43,6 +43,10 @@ exports.createMetadata = async function(ctx) { 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") @@ -66,20 +70,26 @@ exports.createMetadata = async function(ctx) { } } +exports.updateSelfMetadata = async function(ctx) { + // overwrite the ID with current users + ctx.request.body._id = ctx.user.userId + // make sure no stale rev + delete ctx.request.body._rev + await exports.updateMetadata(ctx) +} + 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, - getGlobalIDFromUserMetadataID(user._id), - ctx.request.body - ) + const globalUser = await saveGlobalUser(ctx, appId, { + ...user, + _id: getGlobalIDFromUserMetadataID(user._id), + }) const metadata = { ...globalUser, _id: user._id || generateUserMetadataID(globalUser._id), - _rev: ctx.request.body._rev, + _rev: user._rev, } ctx.body = await db.put(metadata) } diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index bafb648fc6..a9e4aac5a2 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -31,6 +31,12 @@ router usage, controller.createMetadata ) + .post( + "/api/users/metadata/self", + authorized(PermissionTypes.USER, PermissionLevels.WRITE), + usage, + controller.updateSelfMetadata + ) .delete( "/api/users/metadata/:id", authorized(PermissionTypes.USER, PermissionLevels.WRITE), diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index 89c46c6bff..0d75e808ad 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -2,6 +2,10 @@ const { getAppId, setCookie, getCookie, Cookies } = require("@budibase/auth") const { getRole } = require("../utilities/security/roles") const { getGlobalUsers } = require("../utilities/workerRequests") const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles") +const { + getGlobalIDFromUserMetadataID, + generateUserMetadataID, +} = require("../db/utils") module.exports = async (ctx, next) => { // try to get the appID from the request @@ -26,7 +30,8 @@ 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 globalUser = await getGlobalUsers(ctx, requestAppId, ctx.user.email) + const globalId = getGlobalIDFromUserMetadataID(ctx.user.userId) + const globalUser = await getGlobalUsers(ctx, requestAppId, globalId) updateCookie = true appId = requestAppId if (globalUser.roles && globalUser.roles[requestAppId]) { @@ -36,18 +41,24 @@ module.exports = async (ctx, next) => { appId = appCookie.appId roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC } - if (appId) { - ctx.appId = appId - if (roleId) { - ctx.roleId = roleId - ctx.user = { - ...ctx.user, - _id: ctx.user ? ctx.user.userId : null, - role: await getRole(appId, roleId), - } + // nothing more to do + if (!appId) { + return next() + } + + ctx.appId = appId + if (roleId) { + ctx.roleId = roleId + const userId = ctx.user ? generateUserMetadataID(ctx.user.userId) : null + ctx.user = { + ...ctx.user, + // override userID with metadata one + _id: userId, + userId, + role: await getRole(appId, roleId), } } - if (updateCookie && appId) { + if (updateCookie) { setCookie(ctx, { appId, roleId }, Cookies.CurrentApp) } return next() diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index 12dc5e9ff9..2de74aa155 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -91,8 +91,10 @@ exports.getGlobalUsers = async (ctx, appId = null, globalId = null) => { return users } -exports.saveGlobalUser = async (ctx, appId, body, globalId = null) => { - const globalUser = await exports.getGlobalUsers(ctx, appId, globalId) +exports.saveGlobalUser = async (ctx, appId, body) => { + const globalUser = body._id + ? await exports.getGlobalUsers(ctx, appId, body._id) + : {} const roles = globalUser.roles || {} if (body.roleId) { roles[appId] = body.roleId diff --git a/packages/worker/src/api/controllers/admin/groups.js b/packages/worker/src/api/controllers/admin/groups.js index 3642c2464d..86623c337a 100644 --- a/packages/worker/src/api/controllers/admin/groups.js +++ b/packages/worker/src/api/controllers/admin/groups.js @@ -31,15 +31,13 @@ exports.fetch = async function(ctx) { include_docs: true, }) ) - const groups = response.rows.map(row => row.doc) - ctx.body = groups + ctx.body = response.rows.map(row => row.doc) } exports.find = async function(ctx) { const db = new CouchDB(GLOBAL_DB) try { - const record = await db.get(ctx.params.id) - ctx.body = record + ctx.body = await db.get(ctx.params.id) } catch (err) { ctx.throw(err.status, err) } diff --git a/packages/worker/src/api/controllers/admin/users.js b/packages/worker/src/api/controllers/admin/users.js index 600a8e75f6..96243d49fd 100644 --- a/packages/worker/src/api/controllers/admin/users.js +++ b/packages/worker/src/api/controllers/admin/users.js @@ -1,27 +1,42 @@ const CouchDB = require("../../../db") const { hash, - generateUserID, - getUserParams, + generateGlobalUserID, + getGlobalUserParams, StaticDatabases, + getGlobalUserByEmail, } = require("@budibase/auth") const { UserStatus } = require("../../../constants") +const FIRST_USER_EMAIL = "test@test.com" +const FIRST_USER_PASSWORD = "test" 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, + + // make sure another user isn't using the same email + const dbUser = await getGlobalUserByEmail(email) + if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { + ctx.throw(400, "Email address already in use.") } - let dbUser - // in-case user existed already - if (_id) { - dbUser = await db.get(_id) + + // get the password, make sure one is defined + let hashedPassword + if (password) { + hashedPassword = await hash(password) + } else if (dbUser) { + hashedPassword = dbUser.password + } else { + ctx.throw(400, "Password must be specified.") + } + + let user = { + ...dbUser, + ...ctx.request.body, + _id: _id || generateGlobalUserID(), + password: hashedPassword, } // add the active status to a user if its not provided if (user.status == null) { @@ -29,7 +44,7 @@ exports.userSave = async ctx => { } try { const response = await db.post({ - password: hashedPassword || dbUser.password, + password: hashedPassword, ...user, }) ctx.body = { @@ -46,12 +61,24 @@ exports.userSave = async ctx => { } } +exports.firstUser = async ctx => { + ctx.request.body = { + email: FIRST_USER_EMAIL, + password: FIRST_USER_PASSWORD, + roles: {}, + builder: { + global: true, + }, + } + await exports.userSave(ctx) +} + exports.userDelete = async ctx => { const db = new CouchDB(GLOBAL_DB) - const dbUser = await db.get(generateUserID(ctx.params.email)) + const dbUser = await db.get(ctx.params.id) await db.remove(dbUser._id, dbUser._rev) ctx.body = { - message: `User ${ctx.params.email} deleted.`, + message: `User ${ctx.params.id} deleted.`, } } @@ -59,7 +86,7 @@ exports.userDelete = async ctx => { exports.userFetch = async ctx => { const db = new CouchDB(GLOBAL_DB) const response = await db.allDocs( - getUserParams(null, { + getGlobalUserParams(null, { include_docs: true, }) ) @@ -78,7 +105,7 @@ exports.userFind = async ctx => { const db = new CouchDB(GLOBAL_DB) let user try { - user = await db.get(generateUserID(ctx.params.email)) + user = await db.get(ctx.params.id) } catch (err) { // no user found, just return nothing user = {} diff --git a/packages/worker/src/api/routes/admin/users.js b/packages/worker/src/api/routes/admin/users.js index fe8e57593a..d7d19cbf49 100644 --- a/packages/worker/src/api/routes/admin/users.js +++ b/packages/worker/src/api/routes/admin/users.js @@ -32,8 +32,9 @@ router authenticated, controller.userSave ) - .delete("/api/admin/users/:email", authenticated, controller.userDelete) + .post("/api/admin/users/first", controller.firstUser) + .delete("/api/admin/users/:id", authenticated, controller.userDelete) .get("/api/admin/users", authenticated, controller.userFetch) - .get("/api/admin/users/:email", authenticated, controller.userFind) + .get("/api/admin/users/:id", authenticated, controller.userFind) module.exports = router diff --git a/packages/worker/src/db/utils.js b/packages/worker/src/db/utils.js deleted file mode 100644 index b250b895bb..0000000000 --- a/packages/worker/src/db/utils.js +++ /dev/null @@ -1,35 +0,0 @@ -exports.StaticDatabases = { - USER: { - name: "user-db", - }, -} - -const DocumentTypes = { - USER: "us", - APP: "app", -} - -exports.DocumentTypes = DocumentTypes - -const UNICODE_MAX = "\ufff0" -const SEPARATOR = "_" - -/** - * Generates a new user ID based on the passed in email. - * @param {string} email The email which the ID is going to be built up of. - * @returns {string} The new user ID which the user doc can be stored under. - */ -exports.generateUserID = email => { - return `${DocumentTypes.USER}${SEPARATOR}${email}` -} - -/** - * Gets parameters for retrieving users, this is a utility function for the getDocParams function. - */ -exports.getUserParams = (email = "", otherProps = {}) => { - return { - ...otherProps, - startkey: `${DocumentTypes.USER}${SEPARATOR}${email}`, - endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`, - } -}