First pass of global user configuration through existing user API with role mappings.

This commit is contained in:
mike12345567 2021-04-08 16:58:33 +01:00
parent 4abe6192dc
commit 8cde219db9
17 changed files with 206 additions and 104 deletions

View File

@ -26,6 +26,10 @@ static_resources:
cluster: redis-service
prefix_rewrite: "/"
- match: { prefix: "/api/admin" }
route:
cluster: worker-dev
- match: { prefix: "/api/" }
route:
cluster: server-dev
@ -123,3 +127,17 @@ static_resources:
address: {{ address }}
port_value: 3000
- name: worker-dev
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: worker-dev
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: {{ address }}
port_value: 4002

View File

@ -25,6 +25,11 @@ static_resources:
- match: { path: "/" }
route:
cluster: app-service
# special case for worker admin API
- match: { path: "/api/admin" }
route:
cluster: worker-service
# special case for when API requests are made, can just forward, not to minio
- match: { prefix: "/api/" }

View File

@ -32,3 +32,7 @@ exports.getUserParams = (email = "", otherProps = {}) => {
endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`,
}
}
exports.getEmailFromUserID = id => {
return id.split(`${DocumentTypes.USER}${SEPARATOR}`)[1]
}

View File

@ -8,7 +8,7 @@ const { jwt, local, google } = require("./middleware")
const { Cookies, UserStatus } = require("./constants")
const { hash, compare } = require("./hashing")
const { getAppId, setCookie } = require("./utils")
const { generateUserID, getUserParams } = require("./db/utils")
const { generateUserID, getUserParams, getEmailFromUserID } = require("./db/utils")
// Strategies
passport.use(new LocalStrategy(local.options, local.authenticate))
@ -36,6 +36,7 @@ module.exports = {
StaticDatabases,
generateUserID,
getUserParams,
getEmailFromUserID,
hash,
compare,
getAppId,

View File

@ -79,6 +79,7 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.0.1",
"@budibase/client": "^0.8.9",
"@budibase/string-templates": "^0.8.9",
"@elastic/elasticsearch": "7.10.0",

View File

@ -7,7 +7,7 @@ const { generateUserID } = require("../../db/utils")
const { setCookie } = require("../../utilities")
const { outputProcessing } = require("../../utilities/rowProcessor")
const { ViewNames } = require("../../db/utils")
const { UserStatus } = require("../../constants")
const { UserStatus } = require("@budibase/auth")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const INVALID_ERR = "Invalid Credentials"

View File

@ -48,7 +48,7 @@ async function findRow(db, appId, tableId, rowId) {
appId,
},
}
await usersController.find(ctx)
await usersController.findMetadata(ctx)
row = ctx.body
} else {
row = await db.get(rowId)
@ -103,7 +103,7 @@ exports.patch = async function(ctx) {
...row,
password: ctx.request.body.password,
}
await usersController.update(ctx)
await usersController.updateMetadata(ctx)
return
}
@ -179,7 +179,7 @@ exports.save = async function(ctx) {
if (row.tableId === ViewNames.USERS) {
// the row has been updated, need to put it into the ctx
ctx.request.body = row
await usersController.create(ctx)
await usersController.createMetadata(ctx)
return
}
@ -310,7 +310,7 @@ exports.fetchTableRows = async function(ctx) {
let rows,
table = await db.get(ctx.params.tableId)
if (ctx.params.tableId === ViewNames.USERS) {
await usersController.fetch(ctx)
await usersController.fetchMetadata(ctx)
rows = ctx.body
} else {
const response = await db.allDocs(

View File

@ -1,109 +1,137 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
const {
generateUserID,
getUserParams,
getEmailFromUserID,
} = require("@budibase/auth")
const { InternalTables } = require("../../db/utils")
const { getRole } = require("../../utilities/security/roles")
const { UserStatus } = require("../../constants")
const { checkSlashesInUrl } = require("../../utilities")
const env = require("../../environment")
const fetch = require("node-fetch")
exports.fetch = async function(ctx) {
async function deleteGlobalUser(email) {
const endpoint = `/api/admin/users/${email}`
const reqCfg = { method: "DELETE" }
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
reqCfg
)
return response.json()
}
async function getGlobalUsers(email = null) {
const endpoint = email ? `/api/admin/users/${email}` : `/api/admin/users`
const reqCfg = { method: "GET" }
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
reqCfg
)
return response.json()
}
async function saveGlobalUser(appId, email, body) {
const globalUser = await getGlobalUsers(email)
const roles = globalUser.roles || {}
if (body.roleId) {
roles.appId = body.roleId
}
const endpoint = `/api/admin/users`
const reqCfg = {
method: "POST",
body: {
...globalUser,
email,
password: body.password,
status: body.status,
roles,
},
}
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
reqCfg
)
await response.json()
delete body.email
delete body.password
delete body.roleId
delete body.status
return body
}
exports.fetchMetadata = async function(ctx) {
const database = new CouchDB(ctx.appId)
const users = (
const global = await getGlobalUsers()
const metadata = (
await database.allDocs(
getUserParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
// user hashed password shouldn't ever be returned
for (let user of users) {
delete user.password
const users = []
for (let user of global) {
const info = metadata.find(meta => meta._id.includes(user.email))
users.push({
...user,
...info,
})
}
ctx.body = users
}
// TODO: need to replace this with something that purely manages metadata
exports.create = async function(ctx) {
const db = new CouchDB(ctx.appId)
const { email, password, roleId } = ctx.request.body
if (!email || !password) {
ctx.throw(400, "email and Password Required.")
}
const role = await getRole(ctx.appId, roleId)
exports.createMetadata = async function(ctx) {
const appId = ctx.appId
const db = new CouchDB(appId)
const { email, roleId } = ctx.request.body
// check role valid
const role = await getRole(appId, roleId)
if (!role) ctx.throw(400, "Invalid Role")
const hashedPassword = await bcrypt.hash(password)
const metadata = await saveGlobalUser(appId, email, ctx.request.body)
const user = {
...ctx.request.body,
// these must all be after the object spread, make sure
// any values are overwritten, generateUserID will always
// generate the same ID for the user as it is not UUID based
...metadata,
_id: generateUserID(email),
type: "user",
password: hashedPassword,
tableId: ViewNames.USERS,
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = UserStatus.ACTIVE
tableId: InternalTables.USER_METADATA,
}
try {
const response = await db.post(user)
ctx.status = 200
ctx.message = "User created successfully."
ctx.userId = response.id
ctx.body = {
_rev: response.rev,
email,
}
} catch (err) {
if (err.status === 409) {
ctx.throw(400, "User exists already")
} else {
ctx.throw(err.status, err)
}
const response = await db.post(user)
ctx.body = {
_rev: response.rev,
email,
}
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.appId)
exports.updateMetadata = async function(ctx) {
const appId = ctx.appId
const db = new CouchDB(appId)
const user = ctx.request.body
let dbUser
if (user.email && !user._id) {
user._id = generateUserID(user.email)
}
// get user incase password removed
if (user._id) {
dbUser = await db.get(user._id)
}
if (user.password) {
user.password = await bcrypt.hash(user.password)
} else {
delete user.password
}
let email = user.email || getEmailFromUserID(user._id)
const metadata = await saveGlobalUser(appId, email, ctx.request.body)
const response = await db.put({
password: dbUser.password,
...user,
if (!metadata._id) {
user._id = generateUserID(email)
}
ctx.body = await db.put({
...metadata,
})
user._rev = response.rev
ctx.status = 200
ctx.body = response
}
exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.appId)
await database.destroy(generateUserID(ctx.params.email))
exports.destroyMetadata = async function(ctx) {
const db = new CouchDB(ctx.appId)
const email = ctx.params.email
await deleteGlobalUser(email)
await db.destroy(generateUserID(email))
ctx.body = {
message: `User ${ctx.params.email} deleted.`,
}
ctx.status = 200
}
exports.find = async function(ctx) {
exports.findMetadata = async function(ctx) {
const database = new CouchDB(ctx.appId)
let lookup = ctx.params.email
? generateUserID(ctx.params.email)

View File

@ -11,31 +11,31 @@ const router = Router()
router
.get(
"/api/users",
"/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.fetch
controller.fetchMetadata
)
.get(
"/api/users/:email",
"/api/users/metadata/:email",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.find
controller.findMetadata
)
.put(
"/api/users",
"/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
controller.update
controller.updateMetadata
)
.post(
"/api/users",
"/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.create
controller.createMetadata
)
.delete(
"/api/users/:email",
"/api/users/metadata/:email",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.destroy
controller.destroyMetadata
)
module.exports = router

View File

@ -75,7 +75,7 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
if (env.isProd()) {
await usage.update(apiKey, usage.Properties.USER, 1)
}
await userController.create(ctx)
await userController.createMetadata(ctx)
return {
response: ctx.body,
// internal property not returned through the API

View File

@ -1,4 +1,5 @@
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
const { UserStatus } = require("@budibase/auth")
exports.LOGO_URL =
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
@ -27,11 +28,6 @@ exports.AuthTypes = {
EXTERNAL: "external",
}
exports.UserStatus = {
ACTIVE: "active",
INACTIVE: "inactive",
}
exports.USERS_TABLE_SCHEMA = {
_id: "ta_users",
type: "table",
@ -68,7 +64,7 @@ exports.USERS_TABLE_SCHEMA = {
constraints: {
type: exports.FieldTypes.STRING,
presence: false,
inclusion: Object.values(exports.UserStatus),
inclusion: Object.values(UserStatus),
},
},
},

View File

@ -34,7 +34,10 @@ const DocumentTypes = {
const ViewNames = {
LINK: "by_link",
ROUTING: "screen_routes",
USERS: "ta_users",
}
const InternalTables = {
USER_METADATA: "ta_users",
}
const SearchIndexes = {
@ -43,6 +46,7 @@ const SearchIndexes = {
exports.StaticDatabases = StaticDatabases
exports.ViewNames = ViewNames
exports.InternalTables = InternalTables
exports.DocumentTypes = DocumentTypes
exports.SEPARATOR = SEPARATOR
exports.UNICODE_MAX = UNICODE_MAX

View File

@ -279,7 +279,7 @@ class TestConfiguration {
roleId,
},
null,
controllers.user.create
controllers.user.createMetadata
)
}
@ -289,7 +289,7 @@ class TestConfiguration {
{
email,
},
controllers.user.find
controllers.user.findMetadata
)
return this._req(
{
@ -297,7 +297,7 @@ class TestConfiguration {
status: "inactive",
},
null,
controllers.user.update
controllers.user.updateMetadata
)
}

View File

@ -25,7 +25,7 @@
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"got": "^11.8.1",
"joi": "^17.2.1",
"joi": "^17.4.0",
"koa": "^2.7.0",
"koa-body": "^4.2.0",
"koa-compress": "^4.0.1",

View File

@ -3,11 +3,28 @@ const passport = require("@budibase/auth")
const controller = require("../../controllers/admin")
const authController = require("../../controllers/admin/auth")
const authenticated = require("../../../middleware/authenticated")
const joiValidator = require("../../../middleware/joi-validator")
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(),
// maps appId -> roleId for the user
roles: Joi.object()
.pattern(/.*/, Joi.string())
.required()
.unknown(true)
}).required().unknown(true))
}
router
.post("/api/admin/users", authenticated, controller.userSave)
.post("/api/admin/users", buildUserSaveValidation(), authenticated, controller.userSave)
.post("/api/admin/authenticate", authController.authenticate)
.delete("/api/admin/users/:email", authenticated, controller.userDelete)
.get("/api/admin/users", authenticated, controller.userFetch)

View File

@ -0,0 +1,28 @@
function validate(schema, property) {
// Return a Koa middleware function
return (ctx, next) => {
if (!schema) {
return next()
}
let params = null
if (ctx[property] != null) {
params = ctx[property]
} else if (ctx.request[property] != null) {
params = ctx.request[property]
}
const { error } = schema.validate(params)
if (error) {
ctx.throw(400, `Invalid ${property} - ${error.message}`)
return
}
return next()
}
}
module.exports.body = schema => {
return validate(schema, "body")
}
module.exports.params = schema => {
return validate(schema, "params")
}

View File

@ -1261,10 +1261,10 @@ jmespath@0.15.0, jmespath@^0.15.0:
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
joi@^17.2.1:
version "17.3.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
joi@^17.4.0:
version "17.4.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.0.tgz#b5c2277c8519e016316e49ababd41a1908d9ef20"
integrity sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"