First pass of global user configuration through existing user API with role mappings.
This commit is contained in:
parent
4abe6192dc
commit
8cde219db9
|
@ -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
|
||||
|
||||
|
|
|
@ -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/" }
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue