user / rbac events + tests

This commit is contained in:
Rory Powell 2022-04-08 01:28:22 +01:00
parent ac8573b67e
commit e98e659346
41 changed files with 861 additions and 340 deletions

View File

@ -1,8 +1,8 @@
class BudibaseError extends Error { class BudibaseError extends Error {
constructor(message, type, code) { constructor(message, code, type) {
super(message) super(message)
this.type = type
this.code = code this.code = code
this.type = type
} }
} }

View File

@ -0,0 +1,11 @@
const { BudibaseError } = require("./base")
class GenericError extends BudibaseError {
constructor(message, code, type) {
super(message, code, type ? type : "generic")
}
}
module.exports = {
GenericError,
}

View File

@ -0,0 +1,12 @@
const { GenericError } = require("./generic")
class HTTPError extends GenericError {
constructor(message, httpStatus, code, type) {
super(message, code ? code : "http", type)
this.status = httpStatus
}
}
module.exports = {
HTTPError,
}

View File

@ -1,12 +1,11 @@
const http = require("./http")
const licensing = require("./licensing") const licensing = require("./licensing")
const codes = { const codes = {
...licensing.codes, ...licensing.codes,
} }
const types = { const types = [licensing.type]
...licensing.types,
}
const context = { const context = {
...licensing.context, ...licensing.context,
@ -36,6 +35,9 @@ const getPublicError = err => {
module.exports = { module.exports = {
codes, codes,
types, types,
UsageLimitError: licensing.UsageLimitError, errors: {
UsageLimitError: licensing.UsageLimitError,
HTTPError: http.HTTPError,
},
getPublicError, getPublicError,
} }

View File

@ -1,8 +1,6 @@
const { BudibaseError } = require("./base") const { HTTPError } = require("./http")
const types = { const type = "license_error"
LICENSE_ERROR: "license_error",
}
const codes = { const codes = {
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
@ -16,16 +14,15 @@ const context = {
}, },
} }
class UsageLimitError extends BudibaseError { class UsageLimitError extends HTTPError {
constructor(message, limitName) { constructor(message, limitName) {
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED) super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
this.limitName = limitName this.limitName = limitName
this.status = 400
} }
} }
module.exports = { module.exports = {
types, type,
codes, codes,
context, context,
UsageLimitError, UsageLimitError,

View File

@ -59,8 +59,10 @@ exports.Events = {
// ROLE // ROLE
ROLE_CREATED: "role:created", ROLE_CREATED: "role:created",
ROLE_UPDATED: "role:updated",
ROLE_DELETED: "role:deleted", ROLE_DELETED: "role:deleted",
ROLE_ASSIGNED: "role:assigned", ROLE_ASSIGNED: "role:assigned",
ROLE_UNASSIGNED: "role:unassigned",
// APP / CLIENT // APP / CLIENT
CLIENT_SERVED: "client:served", CLIENT_SERVED: "client:served",

View File

@ -4,7 +4,7 @@ const analytics = require("../analytics")
const logEvent = messsage => { const logEvent = messsage => {
const tenantId = getTenantId() const tenantId = getTenantId()
const userId = getTenantId() // TODO const userId = getTenantId() // TODO
console.log(`[tenant=${tenantId}] [user=${userId}] ${messsage}`) console.log(`[audit] [tenant=${tenantId}] [user=${userId}] ${messsage}`)
} }
exports.processEvent = (event, properties) => { exports.processEvent = (event, properties) => {

View File

@ -8,6 +8,7 @@ const license = require("./license")
const layout = require("./layout") const layout = require("./layout")
const org = require("./org") const org = require("./org")
const query = require("./query") const query = require("./query")
const role = require("./role")
const row = require("./screen") const row = require("./screen")
const table = require("./table") const table = require("./table")
const serve = require("./serve") const serve = require("./serve")
@ -25,6 +26,7 @@ module.exports = {
layout, layout,
org, org,
query, query,
role,
row, row,
table, table,
serve, serve,

View File

@ -1,26 +1,31 @@
const events = require("../events") const events = require("../events")
const { Events } = require("../constants") const { Events } = require("../constants")
// TODO
exports.updgraded = () => { exports.updgraded = () => {
const properties = {} const properties = {}
events.processEvent(Events.LICENSE_UPGRADED, properties) events.processEvent(Events.LICENSE_UPGRADED, properties)
} }
// TODO
exports.downgraded = () => { exports.downgraded = () => {
const properties = {} const properties = {}
events.processEvent(Events.LICENSE_DOWNGRADED, properties) events.processEvent(Events.LICENSE_DOWNGRADED, properties)
} }
// TODO
exports.updated = () => { exports.updated = () => {
const properties = {} const properties = {}
events.processEvent(Events.LICENSE_UPDATED, properties) events.processEvent(Events.LICENSE_UPDATED, properties)
} }
// TODO
exports.activated = () => { exports.activated = () => {
const properties = {} const properties = {}
events.processEvent(Events.LICENSE_ACTIVATED, properties) events.processEvent(Events.LICENSE_ACTIVATED, properties)
} }
// TODO
exports.quotaExceeded = (quotaName, value) => { exports.quotaExceeded = (quotaName, value) => {
const properties = { const properties = {
name: quotaName, name: quotaName,

View File

@ -1,23 +1,24 @@
const events = require("../events") const events = require("../events")
const { Events } = require("../constants") const { Events } = require("../constants")
exports.created = () => { /* eslint-disable */
exports.created = (datasource, query) => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_CREATED, properties) events.processEvent(Events.QUERY_CREATED, properties)
} }
exports.updated = () => { exports.updated = (datasource, query) => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_UPDATED, properties) events.processEvent(Events.QUERY_UPDATED, properties)
} }
exports.deleted = () => { exports.deleted = (datasource, query) => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_DELETED, properties) events.processEvent(Events.QUERY_DELETED, properties)
} }
// TODO exports.import = (datasource, importSource, count) => {
exports.import = () => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_IMPORT, properties) events.processEvent(Events.QUERY_IMPORT, properties)
} }
@ -28,8 +29,7 @@ exports.import = () => {
// events.processEvent(Events.QUERY_RUN, properties) // events.processEvent(Events.QUERY_RUN, properties)
// } // }
// TODO exports.previewed = datasource => {
exports.previewed = () => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_PREVIEWED, properties) events.processEvent(Events.QUERY_PREVIEWED, properties)
} }

View File

@ -1,19 +1,29 @@
const events = require("../events") const events = require("../events")
const { Events } = require("../constants") const { Events } = require("../constants")
exports.created = () => { /* eslint-disable */
exports.created = role => {
const properties = {} const properties = {}
events.processEvent(Events.ROLE_CREATED, properties) events.processEvent(Events.ROLE_CREATED, properties)
} }
// TODO exports.updated = role => {
exports.deleted = () => { const properties = {}
events.processEvent(Events.ROLE_UPDATED, properties)
}
exports.deleted = role => {
const properties = {} const properties = {}
events.processEvent(Events.ROLE_DELETED, properties) events.processEvent(Events.ROLE_DELETED, properties)
} }
// TODO exports.assigned = (user, role) => {
exports.assigned = () => {
const properties = {} const properties = {}
events.processEvent(Events.ROLE_ASSIGNED, properties) events.processEvent(Events.ROLE_ASSIGNED, properties)
} }
exports.unassigned = (user, role) => {
const properties = {}
events.processEvent(Events.ROLE_UNASSIGNED, properties)
}

View File

@ -1,85 +1,87 @@
const events = require("../events") const events = require("../events")
const { Events } = require("../constants") const { Events } = require("../constants")
// TODO /* eslint-disable */
exports.created = () => {
exports.created = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_CREATED, properties) events.processEvent(Events.USER_CREATED, properties)
} }
// TODO exports.updated = user => {
exports.updated = () => {
const properties = {} const properties = {}
events.processEvent(Events.USER_UPDATED, properties) events.processEvent(Events.USER_UPDATED, properties)
} }
exports.deleted = () => { exports.deleted = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_DELETED, properties) events.processEvent(Events.USER_DELETED, properties)
} }
// TODO // TODO
exports.passwordForceReset = () => { exports.passwordForceReset = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_PASSWORD_FORCE_RESET, properties) events.processEvent(Events.USER_PASSWORD_FORCE_RESET, properties)
} }
// PERMISSIONS // PERMISSIONS
// TODO exports.permissionAdminAssigned = user => {
exports.permissionAdminAssigned = () => {
const properties = {} const properties = {}
events.processEvent(Events.USER_PERMISSION_ADMIN_ASSIGNED, properties) events.processEvent(Events.USER_PERMISSION_ADMIN_ASSIGNED, properties)
} }
// TODO exports.permissionAdminRemoved = user => {
exports.permissionAdminRemoved = () => {
const properties = {} const properties = {}
events.processEvent(Events.USER_PERMISSION_ADMIN_REMOVED, properties) events.processEvent(Events.USER_PERMISSION_ADMIN_REMOVED, properties)
} }
// TODO exports.permissionBuilderAssigned = user => {
exports.permissionBuilderAssigned = () => {
const properties = {} const properties = {}
events.processEvent(Events.USER_PERMISSION_BUILDER_ASSIGNED, properties) events.processEvent(Events.USER_PERMISSION_BUILDER_ASSIGNED, properties)
} }
// TODO exports.permissionBuilderRemoved = user => {
exports.permissionBuilderRemoved = () => {
const properties = {} const properties = {}
events.processEvent(Events.USER_PERMISSION_BUILDER_REMOVED, properties) events.processEvent(Events.USER_PERMISSION_BUILDER_REMOVED, properties)
} }
// INVITE // INVITE
exports.invited = () => { // TODO
exports.invited = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_INVITED, properties) events.processEvent(Events.USER_INVITED, properties)
} }
exports.inviteAccepted = () => { // TODO
exports.inviteAccepted = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_INVITED_ACCEPTED, properties) events.processEvent(Events.USER_INVITED_ACCEPTED, properties)
} }
// SELF // SELF
exports.selfUpdated = () => { // TODO
exports.selfUpdated = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_SELF_UPDATED, properties) events.processEvent(Events.USER_SELF_UPDATED, properties)
} }
exports.selfPasswordUpdated = () => { // TODO
exports.selfPasswordUpdated = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_SELF_PASSWORD_UPDATED, properties) events.processEvent(Events.USER_SELF_PASSWORD_UPDATED, properties)
} }
exports.passwordResetRequested = () => { // TODO
exports.passwordResetRequested = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_PASSWORD_RESET_REQUESTED, properties) events.processEvent(Events.USER_PASSWORD_RESET_REQUESTED, properties)
} }
exports.passwordReset = () => { // TODO
exports.passwordReset = user => {
const properties = {} const properties = {}
events.processEvent(Events.USER_PASSWORD_RESET, properties) events.processEvent(Events.USER_PASSWORD_RESET, properties)
} }

View File

@ -1,4 +1,5 @@
const db = require("./db") const db = require("./db")
const errors = require("./errors")
module.exports = { module.exports = {
init(opts = {}) { init(opts = {}) {
@ -11,15 +12,19 @@ module.exports = {
redis: require("../redis"), redis: require("../redis"),
objectStore: require("../objectStore"), objectStore: require("../objectStore"),
utils: require("../utils"), utils: require("../utils"),
users: require("./users"),
cache: require("../cache"), cache: require("../cache"),
auth: require("../auth"), auth: require("../auth"),
constants: require("../constants"), constants: require("../constants"),
migrations: require("../migrations"), migrations: require("../migrations"),
errors: require("./errors"), errors: require("./errors"),
...errors.errors,
env: require("./environment"), env: require("./environment"),
accounts: require("./cloud/accounts"), accounts: require("./cloud/accounts"),
tenancy: require("./tenancy"), tenancy: require("./tenancy"),
featureFlags: require("./featureFlags"), featureFlags: require("./featureFlags"),
events: require("./events"), events: require("./events"),
analytics: require("./analytics"), analytics: require("./analytics"),
sessions: require("./security/sessions"),
deprovisioning: require("./context/deprovision"),
} }

View File

@ -2,7 +2,7 @@ const jwt = require("jsonwebtoken")
const { UserStatus } = require("../../constants") const { UserStatus } = require("../../constants")
const { compare } = require("../../hashing") const { compare } = require("../../hashing")
const env = require("../../environment") const env = require("../../environment")
const { getGlobalUserByEmail } = require("../../utils") const users = require("../../users")
const { authError } = require("./utils") const { authError } = require("./utils")
const { newid } = require("../../hashing") const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions") const { createASession } = require("../../security/sessions")
@ -28,7 +28,7 @@ exports.authenticate = async function (ctx, email, password, done) {
if (!email) return authError(done, "Email Required") if (!email) return authError(done, "Email Required")
if (!password) return authError(done, "Password Required") if (!password) return authError(done, "Password Required")
const dbUser = await getGlobalUserByEmail(email) const dbUser = await users.getGlobalUserByEmail(email)
if (dbUser == null) { if (dbUser == null) {
return authError(done, "User not found") return authError(done, "User not found")
} }

View File

@ -4,7 +4,7 @@ const { generateGlobalUserID } = require("../../db/utils")
const { authError } = require("./utils") const { authError } = require("./utils")
const { newid } = require("../../hashing") const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions") const { createASession } = require("../../security/sessions")
const { getGlobalUserByEmail } = require("../../utils") const users = require("../../users")
const { getGlobalDB, getTenantId } = require("../../tenancy") const { getGlobalDB, getTenantId } = require("../../tenancy")
const fetch = require("node-fetch") const fetch = require("node-fetch")
@ -52,7 +52,7 @@ exports.authenticateThirdParty = async function (
// fallback to loading by email // fallback to loading by email
if (!dbUser) { if (!dbUser) {
dbUser = await getGlobalUserByEmail(thirdPartyUser.email) dbUser = await users.getGlobalUserByEmail(thirdPartyUser.email)
} }
// exit early if there is still no user and auto creation is disabled // exit early if there is still no user and auto creation is disabled
@ -81,7 +81,7 @@ exports.authenticateThirdParty = async function (
// create or sync the user // create or sync the user
let response let response
try { try {
response = await saveUserFn(dbUser, getTenantId(), false, false) response = await saveUserFn(dbUser, false, false)
} catch (err) { } catch (err) {
return authError(done, err) return authError(done, err)
} }

View File

@ -62,6 +62,29 @@ jest.mock("../../../events", () => {
import: jest.fn(), import: jest.fn(),
previewed: jest.fn(), previewed: jest.fn(),
}, },
role: {
created: jest.fn(),
updated: jest.fn(),
deleted: jest.fn(),
assigned: jest.fn(),
unassigned: jest.fn(),
},
user: {
created: jest.fn(),
updated: jest.fn(),
deleted: jest.fn(),
passwordForceReset: jest.fn(),
permissionAdminAssigned: jest.fn(),
permissionAdminRemoved: jest.fn(),
permissionBuilderAssigned: jest.fn(),
permissionBuilderRemoved: jest.fn(),
invited: jest.fn(),
inviteAccepted: jest.fn(),
selfUpdated: jest.fn(),
selfPasswordUpdated: jest.fn(),
passwordResetRequested: jest.fn(),
passwordReset: jest.fn(),
},
} }
}) })

View File

@ -0,0 +1,19 @@
const { ViewNames } = require("./db/utils")
const { queryGlobalView } = require("./db/views")
/**
* Given an email address this will use a view to search through
* all the users to find one with this email address.
* @param {string} email the email to lookup the user by.
* @return {Promise<object|null>}
*/
exports.getGlobalUserByEmail = async email => {
if (email == null) {
throw "Must supply an email address to view"
}
return queryGlobalView(ViewNames.USER_BY_EMAIL, {
key: email.toLowerCase(),
include_docs: true,
})
}

View File

@ -1,24 +1,10 @@
const { const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils")
DocumentTypes,
SEPARATOR,
ViewNames,
generateGlobalUserID,
} = require("./db/utils")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt") const { options } = require("./middleware/passport/jwt")
const { queryGlobalView } = require("./db/views") const { queryGlobalView } = require("./db/views")
const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants") const { Headers, Cookies, MAX_VALID_DATE } = require("./constants")
const {
getGlobalDB,
updateTenantId,
getTenantUser,
tryAddTenant,
} = require("./tenancy")
const environment = require("./environment")
const accounts = require("./cloud/accounts")
const { hash } = require("./hashing")
const userCache = require("./cache/user")
const env = require("./environment") const env = require("./environment")
const userCache = require("./cache/user")
const { getUserSessions, invalidateSessions } = require("./security/sessions") const { getUserSessions, invalidateSessions } = require("./security/sessions")
const events = require("./events") const events = require("./events")
@ -106,8 +92,8 @@ exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
overwrite: true, overwrite: true,
} }
if (environment.COOKIE_DOMAIN) { if (env.COOKIE_DOMAIN) {
config.domain = environment.COOKIE_DOMAIN config.domain = env.COOKIE_DOMAIN
} }
ctx.cookies.set(name, value, config) ctx.cookies.set(name, value, config)
@ -130,23 +116,6 @@ exports.isClient = ctx => {
return ctx.headers[Headers.TYPE] === "client" return ctx.headers[Headers.TYPE] === "client"
} }
/**
* Given an email address this will use a view to search through
* all the users to find one with this email address.
* @param {string} email the email to lookup the user by.
* @return {Promise<object|null>}
*/
exports.getGlobalUserByEmail = async email => {
if (email == null) {
throw "Must supply an email address to view"
}
return queryGlobalView(ViewNames.USER_BY_EMAIL, {
key: email.toLowerCase(),
include_docs: true,
})
}
exports.getBuildersCount = async () => { exports.getBuildersCount = async () => {
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, { const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
include_docs: false, include_docs: false,
@ -154,96 +123,6 @@ exports.getBuildersCount = async () => {
return builders.length return builders.length
} }
exports.saveUser = async (
user,
tenantId,
hashPassword = true,
requirePassword = true
) => {
if (!tenantId) {
throw "No tenancy specified."
}
// need to set the context for this request, as specified
updateTenantId(tenantId)
// specify the tenancy incase we're making a new admin user (public)
const db = getGlobalDB(tenantId)
let { email, password, _id } = user
// make sure another user isn't using the same email
let dbUser
if (email) {
// check budibase users inside the tenant
dbUser = await exports.getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
throw `Email address ${email} already in use.`
}
// check budibase users in other tenants
if (env.MULTI_TENANCY) {
const tenantUser = await getTenantUser(email)
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
// check root account users in account portal
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
} else {
dbUser = await db.get(_id)
}
// get the password, make sure one is defined
let hashedPassword
if (password) {
hashedPassword = hashPassword ? await hash(password) : password
} else if (dbUser) {
hashedPassword = dbUser.password
} else if (requirePassword) {
throw "Password must be specified."
}
_id = _id || generateGlobalUserID()
user = {
createdAt: Date.now(),
...dbUser,
...user,
_id,
password: hashedPassword,
tenantId,
}
// make sure the roles object is always present
if (!user.roles) {
user.roles = {}
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = UserStatus.ACTIVE
}
try {
const response = await db.put({
password: hashedPassword,
...user,
})
await tryAddTenant(tenantId, _id, email)
await userCache.invalidateUser(response.id)
return {
_id: response.id,
_rev: response.rev,
email,
}
} catch (err) {
if (err.status === 409) {
throw "User exists already"
} else {
throw err
}
}
}
/** /**
* Logs a user out from budibase. Re-used across account portal and builder. * Logs a user out from budibase. Re-used across account portal and builder.
*/ */

View File

@ -84,7 +84,7 @@ export class RestImporter {
const count = successQueries.length const count = successQueries.length
const importSource = this.source.getImportSource() const importSource = this.source.getImportSource()
const datasource = await db.get(datasourceId) const datasource = await db.get(datasourceId)
events.query.import({ datasource, importSource, count }) events.query.import(datasource, importSource, count)
for (let query of successQueries) { for (let query of successQueries) {
events.query.created(query) events.query.created(query)
} }

View File

@ -109,8 +109,7 @@ describe("Rest Importer", () => {
expect(importResult.errorQueries.length).toBe(0) expect(importResult.errorQueries.length).toBe(0)
expect(importResult.queries.length).toBe(assertions[key].count) expect(importResult.queries.length).toBe(assertions[key].count)
expect(events.query.import).toBeCalledTimes(1) expect(events.query.import).toBeCalledTimes(1)
const eventData = { datasource, importSource: assertions[key].source, count: assertions[key].count} expect(events.query.import).toBeCalledWith(datasource, assertions[key].source, assertions[key].count)
expect(events.query.import).toBeCalledWith(eventData)
jest.clearAllMocks() jest.clearAllMocks()
} }

View File

@ -216,9 +216,12 @@ const removeDynamicVariables = async (queryId: any) => {
export async function destroy(ctx: any) { export async function destroy(ctx: any) {
const db = getAppDB() const db = getAppDB()
await removeDynamicVariables(ctx.params.queryId) const queryId = ctx.params.queryId
await removeDynamicVariables(queryId)
const query = await db.get(queryId)
const datasource = await db.get(query.datasourceId)
await db.remove(ctx.params.queryId, ctx.params.revId) await db.remove(ctx.params.queryId, ctx.params.revId)
ctx.message = `Query deleted.` ctx.message = `Query deleted.`
ctx.status = 200 ctx.status = 200
events.query.deleted() events.query.deleted(datasource, query)
} }

View File

@ -10,6 +10,7 @@ const {
InternalTables, InternalTables,
} = require("../../db/utils") } = require("../../db/utils")
const { getAppDB } = require("@budibase/backend-core/context") const { getAppDB } = require("@budibase/backend-core/context")
const { events } = require("@budibase/backend-core")
const UpdateRolesOptions = { const UpdateRolesOptions = {
CREATED: "created", CREATED: "created",
@ -50,8 +51,10 @@ exports.find = async function (ctx) {
exports.save = async function (ctx) { exports.save = async function (ctx) {
const db = getAppDB() const db = getAppDB()
let { _id, name, inherits, permissionId } = ctx.request.body let { _id, name, inherits, permissionId } = ctx.request.body
let isCreate = false
if (!_id) { if (!_id) {
_id = generateRoleID() _id = generateRoleID()
isCreate = true
} else if (isBuiltin(_id)) { } else if (isBuiltin(_id)) {
ctx.throw(400, "Cannot update builtin roles.") ctx.throw(400, "Cannot update builtin roles.")
} }
@ -62,6 +65,11 @@ exports.save = async function (ctx) {
role._rev = ctx.request.body._rev role._rev = ctx.request.body._rev
} }
const result = await db.put(role) const result = await db.put(role)
if (isCreate) {
events.role.created(role)
} else {
events.role.updated(role)
}
await updateRolesOnUserTable(db, _id, UpdateRolesOptions.CREATED) await updateRolesOnUserTable(db, _id, UpdateRolesOptions.CREATED)
role._rev = result.rev role._rev = result.rev
ctx.body = role ctx.body = role
@ -71,6 +79,7 @@ exports.save = async function (ctx) {
exports.destroy = async function (ctx) { exports.destroy = async function (ctx) {
const db = getAppDB() const db = getAppDB()
const roleId = ctx.params.roleId const roleId = ctx.params.roleId
const role = await db.get(roleId)
if (isBuiltin(roleId)) { if (isBuiltin(roleId)) {
ctx.throw(400, "Cannot delete builtin role.") ctx.throw(400, "Cannot delete builtin role.")
} }
@ -88,6 +97,7 @@ exports.destroy = async function (ctx) {
} }
await db.remove(roleId, ctx.params.rev) await db.remove(roleId, ctx.params.rev)
events.role.deleted(role)
await updateRolesOnUserTable( await updateRolesOnUserTable(
db, db,
ctx.params.roleId, ctx.params.roleId,

View File

@ -189,6 +189,7 @@ describe("/queries", () => {
expect(res.body).toEqual([]) expect(res.body).toEqual([])
expect(events.query.deleted).toBeCalledTimes(1) expect(events.query.deleted).toBeCalledTimes(1)
expect(events.query.deleted).toBeCalledWith(datasource, query)
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {

View File

@ -4,6 +4,7 @@ const {
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const setup = require("./utilities") const setup = require("./utilities")
const { basicRole } = setup.structures const { basicRole } = setup.structures
const { events } = require("@budibase/backend-core")
describe("/roles", () => { describe("/roles", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -15,20 +16,48 @@ describe("/roles", () => {
await config.init() await config.init()
}) })
const createRole = async (role) => {
if (!role) {
role = basicRole()
}
return request
.post(`/api/roles`)
.send(role)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
describe("create", () => { describe("create", () => {
it("returns a success message when role is successfully created", async () => { it("returns a success message when role is successfully created", async () => {
const res = await request const res = await createRole()
.post(`/api/roles`)
.send(basicRole())
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.res.statusMessage).toEqual( expect(res.res.statusMessage).toEqual(
"Role 'NewRole' created successfully." "Role 'NewRole' created successfully."
) )
expect(res.body._id).toBeDefined() expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined() expect(res.body._rev).toBeDefined()
expect(events.role.updated).not.toBeCalled()
expect(events.role.created).toBeCalledTimes(1)
expect(events.role.created).toBeCalledWith(res.body)
})
})
describe("update", () => {
it("updates a role", async () => {
let res = await createRole()
jest.clearAllMocks()
res = await createRole(res.body)
expect(res.res.statusMessage).toEqual(
"Role 'NewRole' created successfully."
)
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
expect(events.role.created).not.toBeCalled()
expect(events.role.updated).toBeCalledTimes(1)
expect(events.role.updated).toBeCalledWith(res.body)
}) })
}) })
@ -80,8 +109,10 @@ describe("/roles", () => {
it("should delete custom roles", async () => { it("should delete custom roles", async () => {
const customRole = await config.createRole({ const customRole = await config.createRole({
name: "user", name: "user",
permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY,
inherits: BUILTIN_ROLE_IDS.BASIC,
}) })
delete customRole._rev_tree
await request await request
.delete(`/api/roles/${customRole._id}/${customRole._rev}`) .delete(`/api/roles/${customRole._id}/${customRole._rev}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
@ -90,6 +121,8 @@ describe("/roles", () => {
.get(`/api/roles/${customRole._id}`) .get(`/api/roles/${customRole._id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect(404) .expect(404)
expect(events.role.deleted).toBeCalledTimes(1)
expect(events.role.deleted).toBeCalledWith(customRole)
}) })
}) })
}) })

View File

@ -4,14 +4,7 @@ const { google } = require("@budibase/backend-core/middleware")
const { oidc } = require("@budibase/backend-core/middleware") const { oidc } = require("@budibase/backend-core/middleware")
const { Configs, EmailTemplatePurpose } = require("../../../constants") const { Configs, EmailTemplatePurpose } = require("../../../constants")
const { sendEmail, isEmailConfigured } = require("../../../utilities/email") const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
const { const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils
setCookie,
getCookie,
clearCookie,
getGlobalUserByEmail,
hash,
platformLogout,
} = core.utils
const { Cookies, Headers } = core.constants const { Cookies, Headers } = core.constants
const { passport } = core.auth const { passport } = core.auth
const { checkResetPasswordCode } = require("../../../utilities/redis") const { checkResetPasswordCode } = require("../../../utilities/redis")
@ -21,8 +14,8 @@ const {
isMultiTenant, isMultiTenant,
} = require("@budibase/backend-core/tenancy") } = require("@budibase/backend-core/tenancy")
const env = require("../../../environment") const env = require("../../../environment")
import { users } from "@budibase/pro" const { events, users: usersCore } = require("@budibase/backend-core")
const { events } = require("@budibase/backend-core") import { users } from "../../../sdk"
const ssoCallbackUrl = async (config: any, type: any) => { const ssoCallbackUrl = async (config: any, type: any) => {
// incase there is a callback URL from before // incase there is a callback URL from before
@ -112,7 +105,7 @@ export const reset = async (ctx: any) => {
) )
} }
try { try {
const user = await getGlobalUserByEmail(email) const user = await usersCore.getGlobalUserByEmail(email)
// only if user exists, don't error though if they don't // only if user exists, don't error though if they don't
if (user) { if (user) {
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {

View File

@ -7,7 +7,7 @@ const {
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const { user: userCache } = require("@budibase/backend-core/cache") const { user: userCache } = require("@budibase/backend-core/cache")
const { getGlobalDB } = require("@budibase/backend-core/tenancy") const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const { allUsers } = require("../../utilities") const { users } = require("../../../sdk")
exports.fetch = async ctx => { exports.fetch = async ctx => {
const tenantId = ctx.user.tenantId const tenantId = ctx.user.tenantId
@ -49,10 +49,10 @@ exports.find = async ctx => {
exports.removeAppRole = async ctx => { exports.removeAppRole = async ctx => {
const { appId } = ctx.params const { appId } = ctx.params
const db = getGlobalDB() const db = getGlobalDB()
const users = await allUsers(ctx) const allUsers = await users.allUsers(ctx)
const bulk = [] const bulk = []
const cacheInvalidations = [] const cacheInvalidations = []
for (let user of users) { for (let user of allUsers) {
if (user.roles[appId]) { if (user.roles[appId]) {
cacheInvalidations.push(userCache.invalidateUser(user._id)) cacheInvalidations.push(userCache.invalidateUser(user._id))
delete user.roles[appId] delete user.roles[appId]

View File

@ -13,7 +13,7 @@ const {
} = require("@budibase/backend-core/utils") } = require("@budibase/backend-core/utils")
const { encrypt } = require("@budibase/backend-core/encryption") const { encrypt } = require("@budibase/backend-core/encryption")
const { newid } = require("@budibase/backend-core/utils") const { newid } = require("@budibase/backend-core/utils")
const { getUser } = require("../../utilities") const { users } = require("../../../sdk")
const { Cookies } = require("@budibase/backend-core/constants") const { Cookies } = require("@budibase/backend-core/constants")
function newApiKey() { function newApiKey() {
@ -103,7 +103,7 @@ exports.getSelf = async ctx => {
checkCurrentApp(ctx) checkCurrentApp(ctx)
// get the main body of the user // get the main body of the user
ctx.body = await getUser(userId) ctx.body = await users.getUser(userId)
addSessionAttributesToUser(ctx) addSessionAttributesToUser(ctx)
} }

View File

@ -1,32 +1,18 @@
const {
getGlobalUserParams,
StaticDatabases,
} = require("@budibase/backend-core/db")
const { getGlobalUserByEmail } = require("@budibase/backend-core/utils")
import { EmailTemplatePurpose } from "../../../constants" import { EmailTemplatePurpose } from "../../../constants"
import { checkInviteCode } from "../../../utilities/redis" import { checkInviteCode } from "../../../utilities/redis"
import { sendEmail } from "../../../utilities/email" import { sendEmail } from "../../../utilities/email"
const { user: userCache } = require("@budibase/backend-core/cache") import { users } from "../../../sdk"
const { invalidateSessions } = require("@budibase/backend-core/sessions")
const accounts = require("@budibase/backend-core/accounts")
const { const {
getGlobalDB, errors,
getTenantId, users: usersCore,
getTenantUser, tenancy,
doesTenantExist, db: dbUtils,
} = require("@budibase/backend-core/tenancy") } = require("@budibase/backend-core")
const { removeUserFromInfoDB } = require("@budibase/backend-core/deprovision")
import env from "../../../environment"
import { syncUserInApps } from "../../../utilities/appService"
import { quotas, users } from "@budibase/pro"
const { errors } = require("@budibase/backend-core")
import { allUsers, getUser } from "../../utilities"
export const save = async (ctx: any) => { export const save = async (ctx: any) => {
try { try {
const user: any = await users.save(ctx.request.body, getTenantId()) const user = await users.save(ctx.request.body)
// let server know to sync user
await syncUserInApps(user._id)
ctx.body = user ctx.body = user
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
@ -45,36 +31,19 @@ export const adminUser = async (ctx: any) => {
// account portal sends no password for SSO users // account portal sends no password for SSO users
const requirePassword = parseBooleanParam(ctx.request.query.requirePassword) const requirePassword = parseBooleanParam(ctx.request.query.requirePassword)
if (await doesTenantExist(tenantId)) { if (await tenancy.doesTenantExist(tenantId)) {
ctx.throw(403, "Organisation already exists.") ctx.throw(403, "Organisation already exists.")
} }
const db = getGlobalDB(tenantId) const db = tenancy.getGlobalDB(tenantId)
const response = await db.allDocs( const response = await db.allDocs(
getGlobalUserParams(null, { dbUtils.getGlobalUserParams(null, {
include_docs: true, include_docs: true,
}) })
) )
// write usage quotas for cloud
if (!env.SELF_HOSTED) {
// could be a scenario where it exists, make sure its clean
try {
const usageQuota = await db.get(StaticDatabases.GLOBAL.docs.usageQuota)
if (usageQuota) {
await db.remove(usageQuota._id, usageQuota._rev)
}
} catch (err) {
// don't worry about errors
}
await db.put(quotas.generateNewQuotaUsage())
}
if (response.rows.some((row: any) => row.doc.admin)) { if (response.rows.some((row: any) => row.doc.admin)) {
ctx.throw( ctx.throw(403, "You cannot initialise once a global user has been created.")
403,
"You cannot initialise once an global user has been created."
)
} }
const user = { const user = {
@ -91,44 +60,25 @@ export const adminUser = async (ctx: any) => {
tenantId, tenantId,
} }
try { try {
ctx.body = await users.save(user, tenantId, hashPassword, requirePassword) ctx.body = await tenancy.doInTenant(tenantId, async () => {
return users.save(user, hashPassword, requirePassword)
})
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
} }
} }
export const destroy = async (ctx: any) => { export const destroy = async (ctx: any) => {
const db = getGlobalDB() const id = ctx.params.id
const dbUser = await db.get(ctx.params.id) await users.destroy(id, ctx.user)
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase
const email = dbUser.email
const account = await accounts.getAccount(email)
if (account) {
if (email === ctx.user.email) {
ctx.throw(400, 'Please visit "Account" to delete this user')
} else {
ctx.throw(400, "Account holder cannot be deleted")
}
}
}
await removeUserFromInfoDB(dbUser)
await db.remove(dbUser._id, dbUser._rev)
await quotas.removeUser(dbUser)
await userCache.invalidateUser(dbUser._id)
await invalidateSessions(dbUser._id)
// let server know to sync user
await syncUserInApps(dbUser._id)
ctx.body = { ctx.body = {
message: `User ${ctx.params.id} deleted.`, message: `User ${id} deleted.`,
} }
} }
// called internally by app server user fetch // called internally by app server user fetch
export const fetch = async (ctx: any) => { export const fetch = async (ctx: any) => {
const all = await allUsers() const all = await users.allUsers()
// user hashed password shouldn't ever be returned // user hashed password shouldn't ever be returned
for (let user of all) { for (let user of all) {
if (user) { if (user) {
@ -140,12 +90,12 @@ export const fetch = async (ctx: any) => {
// called internally by app server user find // called internally by app server user find
export const find = async (ctx: any) => { export const find = async (ctx: any) => {
ctx.body = await getUser(ctx.params.id) ctx.body = await users.getUser(ctx.params.id)
} }
export const tenantUserLookup = async (ctx: any) => { export const tenantUserLookup = async (ctx: any) => {
const id = ctx.params.id const id = ctx.params.id
const user = await getTenantUser(id) const user = await tenancy.getTenantUser(id)
if (user) { if (user) {
ctx.body = user ctx.body = user
} else { } else {
@ -155,14 +105,14 @@ export const tenantUserLookup = async (ctx: any) => {
export const invite = async (ctx: any) => { export const invite = async (ctx: any) => {
let { email, userInfo } = ctx.request.body let { email, userInfo } = ctx.request.body
const existing = await getGlobalUserByEmail(email) const existing = await usersCore.getGlobalUserByEmail(email)
if (existing) { if (existing) {
ctx.throw(400, "Email address already in use.") ctx.throw(400, "Email address already in use.")
} }
if (!userInfo) { if (!userInfo) {
userInfo = {} userInfo = {}
} }
userInfo.tenantId = getTenantId() userInfo.tenantId = tenancy.getTenantId()
const opts: any = { const opts: any = {
subject: "{{ company }} platform invitation", subject: "{{ company }} platform invitation",
info: userInfo, info: userInfo,
@ -178,16 +128,15 @@ export const inviteAccept = async (ctx: any) => {
try { try {
// info is an extension of the user object that was stored by global // info is an extension of the user object that was stored by global
const { email, info }: any = await checkInviteCode(inviteCode) const { email, info }: any = await checkInviteCode(inviteCode)
ctx.body = await users.save( ctx.body = await tenancy.doInTenant(info.tenantId, () => {
{ return users.save({
firstName, firstName,
lastName, lastName,
password, password,
email, email,
...info, ...info,
}, })
info.tenantId })
)
} catch (err: any) { } catch (err: any) {
if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
// explicitly re-throw limit exceeded errors // explicitly re-throw limit exceeded errors

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../../controllers/global/self") const controller = require("../../controllers/global/self")
const builderOnly = require("../../../middleware/builderOnly") const builderOnly = require("../../../middleware/builderOnly")
const { buildUserSaveValidation } = require("../../utilities/validation") const { users } = require("../validation")
const router = Router() const router = Router()
@ -11,7 +11,7 @@ router
.get("/api/global/self", controller.getSelf) .get("/api/global/self", controller.getSelf)
.post( .post(
"/api/global/self", "/api/global/self",
buildUserSaveValidation(true), users.buildUserSaveValidation(true),
controller.updateSelf controller.updateSelf
) )

View File

@ -4,7 +4,7 @@ const joiValidator = require("../../../middleware/joi-validator")
const adminOnly = require("../../../middleware/adminOnly") const adminOnly = require("../../../middleware/adminOnly")
const Joi = require("joi") const Joi = require("joi")
const cloudRestricted = require("../../../middleware/cloudRestricted") const cloudRestricted = require("../../../middleware/cloudRestricted")
const { buildUserSaveValidation } = require("../../utilities/validation") const { users } = require("../validation")
const selfController = require("../../controllers/global/self") const selfController = require("../../controllers/global/self")
const router = Router() const router = Router()
@ -41,7 +41,7 @@ router
.post( .post(
"/api/global/users", "/api/global/users",
adminOnly, adminOnly,
buildUserSaveValidation(), users.buildUserSaveValidation(),
controller.save controller.save
) )
.get("/api/global/users", adminOnly, controller.fetch) .get("/api/global/users", adminOnly, controller.fetch)
@ -72,7 +72,7 @@ router
.get("/api/global/users/self", selfController.getSelf) .get("/api/global/users/self", selfController.getSelf)
.post( .post(
"/api/global/users/self", "/api/global/users/self",
buildUserSaveValidation(true), users.buildUserSaveValidation(true),
selfController.updateSelf selfController.updateSelf
) )

View File

@ -1,6 +1,7 @@
jest.mock("nodemailer") jest.mock("nodemailer")
const { config, request, mocks } = require("../../../tests") const { config, request, mocks, structures } = require("../../../tests")
const sendMailMock = mocks.email.mock() const sendMailMock = mocks.email.mock()
const { events } = require("@budibase/backend-core")
describe("/api/global/users", () => { describe("/api/global/users", () => {
let code let code
@ -48,4 +49,258 @@ describe("/api/global/users", () => {
expect(user).toBeDefined() expect(user).toBeDefined()
expect(user._id).toEqual(res.body._id) expect(user._id).toEqual(res.body._id)
}) })
const createUser = async (user) => {
const existing = await config.getUser(user.email)
if (existing) {
await deleteUser(existing._id)
}
return saveUser(user)
}
const updateUser = async (user) => {
const existing = await config.getUser(user.email)
user._id = existing._id
return saveUser(user)
}
const saveUser = async (user) => {
const res = await request
.post(`/api/global/users`)
.send(user)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
// .expect(200)
return res.body
}
const deleteUser = async (email) => {
const user = await config.getUser(email)
if (user) {
await request
.delete(`/api/global/users/${user._id}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
}
describe("create", () => {
it("should be able to create a basic user", async () => {
jest.clearAllMocks()
const user = structures.users.user({ email: "basic@test.com" })
await createUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to create an admin user", async () => {
jest.clearAllMocks()
const user = structures.users.adminUser({ email: "admin@test.com" })
await createUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
})
it("should be able to create a builder user", async () => {
jest.clearAllMocks()
const user = structures.users.builderUser({ email: "builder@test.com" })
await createUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to assign app roles", async () => {
jest.clearAllMocks()
const user = structures.users.user({ email: "assign-roles@test.com" })
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await createUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.role.assigned).toBeCalledTimes(2)
expect(events.role.assigned).toBeCalledWith("role1")
expect(events.role.assigned).toBeCalledWith("role2")
})
})
describe("update", () => {
it("should be able to update a basic user", async () => {
let user = structures.users.user({ email: "basic-update@test.com" })
await createUser(user)
jest.clearAllMocks()
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to update a basic user to an admin user", async () => {
let user = structures.users.user({ email: "basic-update-admin@test.com" })
await createUser(user)
jest.clearAllMocks()
await updateUser(structures.users.adminUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
})
it("should be able to update a basic user to a builder user", async () => {
let user = structures.users.user({ email: "basic-update-builder@test.com" })
await createUser(user)
jest.clearAllMocks()
await updateUser(structures.users.builderUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to update an admin user to a basic user", async () => {
let user = structures.users.adminUser({ email: "admin-update-basic@test.com" })
await createUser(user)
jest.clearAllMocks()
user.admin.global = false
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
})
it("should be able to update an builder user to a basic user", async () => {
let user = structures.users.builderUser({ email: "builder-update-basic@test.com" })
await createUser(user)
jest.clearAllMocks()
user.builder.global = false
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should be able to assign app roles", async () => {
const user = structures.users.user({ email: "assign-roles-update@test.com" })
await createUser(user)
jest.clearAllMocks()
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.assigned).toBeCalledTimes(2)
expect(events.role.assigned).toBeCalledWith("role1")
expect(events.role.assigned).toBeCalledWith("role2")
})
it("should be able to unassign app roles", async () => {
const user = structures.users.user({ email: "unassign-roles@test.com" })
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await createUser(user)
jest.clearAllMocks()
user.roles = {}
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledTimes(2)
expect(events.role.unassigned).toBeCalledWith("role1")
expect(events.role.unassigned).toBeCalledWith("role2")
})
it("should be able to update existing app roles", async () => {
const user = structures.users.user({ email: "update-roles@test.com" })
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await createUser(user)
jest.clearAllMocks()
user.roles = {
"app_123": "role1",
"app_456": "role2-edit",
}
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledWith("role2")
expect(events.role.assigned).toBeCalledTimes(1)
expect(events.role.assigned).toBeCalledWith("role2-edit")
})
})
describe("destroy", () => {
it("should be able to destroy a basic user", async () => {
let user = structures.users.user({ email: "destroy@test.com" })
await createUser(user)
jest.clearAllMocks()
await deleteUser(user.email)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should be able to destroy an admin user", async () => {
let user = structures.users.adminUser({ email: "destroy-admin@test.com" })
await createUser(user)
jest.clearAllMocks()
await deleteUser(user.email)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
})
it("should be able to destroy a builder user", async () => {
let user = structures.users.builderUser({ email: "destroy-admin@test.com" })
await createUser(user)
jest.clearAllMocks()
await deleteUser(user.email)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
})
}) })

View File

@ -0,0 +1 @@
export * as users from "./users"

View File

@ -1,8 +1,8 @@
const joiValidator = require("../../middleware/joi-validator") import joiValidator from "../../../middleware/joi-validator"
const Joi = require("joi") import Joi from "joi"
exports.buildUserSaveValidation = (isSelf = false) => { export const buildUserSaveValidation = (isSelf = false) => {
let schema = { let schema: any = {
email: Joi.string().allow(null, ""), email: Joi.string().allow(null, ""),
password: Joi.string().allow(null, ""), password: Joi.string().allow(null, ""),
forceResetPassword: Joi.boolean().optional(), forceResetPassword: Joi.boolean().optional(),

View File

@ -1,33 +0,0 @@
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const { getGlobalUserParams } = require("@budibase/backend-core/db")
/**
* Retrieves all users from the current tenancy.
*/
exports.allUsers = async () => {
const db = getGlobalDB()
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
})
)
return response.rows.map(row => row.doc)
}
/**
* Gets a user by ID from the global database, based on the current tenancy.
*/
exports.getUser = async userId => {
const db = getGlobalDB()
let user
try {
user = await db.get(userId)
} catch (err) {
// no user found, just return nothing
user = {}
}
if (user) {
delete user.password
}
return user
}

View File

@ -0,0 +1 @@
export * as users from "./users"

View File

@ -0,0 +1,136 @@
const { events } = require("@budibase/backend-core")
export const handleDeleteEvents = (user: any) => {
events.user.deleted(user)
if (isBuilder(user)) {
events.user.permissionBuilderRemoved(user)
}
if (isAdmin(user)) {
events.user.permissionAdminRemoved(user)
}
}
const assignAppRoleEvents = (roles: any, existingRoles: any) => {
for (const [appId, role] of Object.entries(roles)) {
// app role in existing is not same as new
if (!existingRoles || existingRoles[appId] !== role) {
events.role.assigned(role)
}
}
}
const unassignAppRoleEvents = (roles: any, existingRoles: any) => {
if (!existingRoles) {
return
}
for (const [appId, role] of Object.entries(existingRoles)) {
// app role in new is not same as existing
if (!roles || roles[appId] !== role) {
events.role.unassigned(role)
}
}
}
const handleAppRoleEvents = (user: any, existingUser: any) => {
const roles = user.roles
const existingRoles = existingUser?.roles
assignAppRoleEvents(roles, existingRoles)
unassignAppRoleEvents(roles, existingRoles)
}
export const handleSaveEvents = (user: any, existingUser: any) => {
if (existingUser) {
events.user.updated(user)
if (isRemovingBuilder(user, existingUser)) {
events.user.permissionBuilderRemoved(user)
}
if (isRemovingAdmin(user, existingUser)) {
events.user.permissionAdminRemoved(user)
}
} else {
events.user.created(user)
}
if (isAddingBuilder(user, existingUser)) {
events.user.permissionBuilderAssigned(user)
}
if (isAddingAdmin(user, existingUser)) {
events.user.permissionAdminAssigned(user)
}
handleAppRoleEvents(user, existingUser)
}
const isBuilder = (user: any) => user.builder && user.builder.global
const isAdmin = (user: any) => user.admin && user.admin.global
export const isAddingBuilder = (user: any, existingUser: any) => {
return isAddingPermission(user, existingUser, isBuilder)
}
export const isRemovingBuilder = (user: any, existingUser: any) => {
return isRemovingPermission(user, existingUser, isBuilder)
}
const isAddingAdmin = (user: any, existingUser: any) => {
return isAddingPermission(user, existingUser, isAdmin)
}
const isRemovingAdmin = (user: any, existingUser: any) => {
return isRemovingPermission(user, existingUser, isAdmin)
}
/**
* Check if a permission is being added to a new or existing user.
*/
const isAddingPermission = (
user: any,
existingUser: any,
hasPermission: any
) => {
// new user doesn't have the permission
if (!hasPermission(user)) {
return false
}
// existing user has the permission
if (existingUser && hasPermission(existingUser)) {
return false
}
// permission is being added
return true
}
/**
* Check if a permission is being removed from an existing user.
*/
const isRemovingPermission = (
user: any,
existingUser: any,
hasPermission: any
) => {
// new user has the permission
if (hasPermission(user)) {
return false
}
// no existing user or existing user doesn't have the permission
if (!existingUser) {
return false
}
// existing user doesn't have the permission
if (!hasPermission(existingUser)) {
return false
}
// permission is being removed
return true
}

View File

@ -0,0 +1 @@
export * from "./users"

View File

@ -0,0 +1,180 @@
import env from "../../environment"
import { quotas } from "@budibase/pro"
import * as apps from "../../utilities/appService"
const { events } = require("@budibase/backend-core")
import * as eventHelpers from "./events"
const {
tenancy,
accounts,
utils,
db: dbUtils,
constants,
cache,
users: usersCore,
deprovisioning,
sessions,
HTTPError,
} = require("@budibase/backend-core")
/**
* Retrieves all users from the current tenancy.
*/
export const allUsers = async () => {
const db = tenancy.getGlobalDB()
const response = await db.allDocs(
dbUtils.getGlobalUserParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
/**
* Gets a user by ID from the global database, based on the current tenancy.
*/
export const getUser = async (userId: string) => {
const db = tenancy.getGlobalDB()
let user
try {
user = await db.get(userId)
} catch (err: any) {
// no user found, just return nothing
if (err.status === 404) {
return {}
}
throw err
}
if (user) {
delete user.password
}
return user
}
export const save = async (
user: any,
hashPassword = true,
requirePassword = true
) => {
const tenantId = tenancy.getTenantId()
// specify the tenancy incase we're making a new admin user (public)
const db = tenancy.getGlobalDB(tenantId)
let { email, password, _id } = user
// make sure another user isn't using the same email
let dbUser
if (email) {
// check budibase users inside the tenant
dbUser = await usersCore.getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
throw `Email address ${email} already in use.`
}
// check budibase users in other tenants
if (env.MULTI_TENANCY) {
const tenantUser = await tenancy.getTenantUser(email)
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
// check root account users in account portal
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
} else if (_id) {
dbUser = await db.get(_id)
}
// get the password, make sure one is defined
let hashedPassword
if (password) {
hashedPassword = hashPassword ? await utils.hash(password) : password
} else if (dbUser) {
hashedPassword = dbUser.password
} else if (requirePassword) {
throw "Password must be specified."
}
if (!_id) {
_id = dbUtils.generateGlobalUserID(email)
}
user = {
createdAt: Date.now(),
...dbUser,
...user,
_id,
password: hashedPassword,
tenantId,
}
// make sure the roles object is always present
if (!user.roles) {
user.roles = {}
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = constants.UserStatus.ACTIVE
}
try {
// save the user to db
let response
const putUserFn = () => {
return db.put(user)
}
if (await eventHelpers.isAddingBuilder(user, dbUser)) {
response = await quotas.addDeveloper(putUserFn)
} else {
response = await putUserFn()
}
eventHelpers.handleSaveEvents(user, dbUser)
await tenancy.tryAddTenant(tenantId, _id, email)
await cache.user.invalidateUser(response.id)
// let server know to sync user
await apps.syncUserInApps(user._id)
return {
_id: response.id,
_rev: response.rev,
email,
}
} catch (err: any) {
if (err.status === 409) {
throw "User exists already"
} else {
throw err
}
}
}
export const destroy = async (id: string, currentUser: any) => {
const db = tenancy.getGlobalDB()
const dbUser = await db.get(id)
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase
const email = dbUser.email
const account = await accounts.getAccount(email)
if (account) {
if (email === currentUser.email) {
throw new HTTPError('Please visit "Account" to delete this user', 400)
} else {
throw new HTTPError("Account holder cannot be deleted", 400)
}
}
}
await deprovisioning.removeUserFromInfoDB(dbUser)
await db.remove(dbUser._id, dbUser._rev)
eventHelpers.handleDeleteEvents(dbUser)
await quotas.removeUser(dbUser)
await cache.user.invalidateUser(dbUser._id)
await sessions.invalidateSessions(dbUser._id)
// let server know to sync user
await apps.syncUserInApps(dbUser._id)
}

View File

@ -6,7 +6,7 @@ const supertest = require("supertest")
const { jwt } = require("@budibase/backend-core/auth") const { jwt } = require("@budibase/backend-core/auth")
const { Cookies, Headers } = require("@budibase/backend-core/constants") const { Cookies, Headers } = require("@budibase/backend-core/constants")
const { Configs } = require("../constants") const { Configs } = require("../constants")
const { getGlobalUserByEmail } = require("@budibase/backend-core/utils") const { users } = require("@budibase/backend-core")
const { createASession } = require("@budibase/backend-core/sessions") const { createASession } = require("@budibase/backend-core/sessions")
const { TENANT_ID, CSRF_TOKEN } = require("./structures") const { TENANT_ID, CSRF_TOKEN } = require("./structures")
const structures = require("./structures") const structures = require("./structures")
@ -112,20 +112,17 @@ class TestConfiguration {
async getUser(email) { async getUser(email) {
return doInTenant(TENANT_ID, () => { return doInTenant(TENANT_ID, () => {
return getGlobalUserByEmail(email) return users.getGlobalUserByEmail(email)
}) })
} }
async createUser(email = "test@test.com", password = "test") { async createUser(email, password) {
const user = await this.getUser(email) const user = await this.getUser(structures.users.email)
if (user) { if (user) {
return user return user
} }
await this._req( await this._req(
{ structures.users.user({ email, password }),
email,
password,
},
null, null,
controllers.users.save controllers.users.save
) )
@ -133,11 +130,7 @@ class TestConfiguration {
async saveAdminUser() { async saveAdminUser() {
await this._req( await this._req(
{ structures.users.user({ tenantId: TENANT_ID }),
email: "testuser@test.com",
password: "test@test.com",
tenantId: TENANT_ID,
},
null, null,
controllers.users.adminUser controllers.users.adminUser
) )

View File

@ -1,10 +1,12 @@
const configs = require("./configs") const configs = require("./configs")
const users = require("./users")
const TENANT_ID = "default" const TENANT_ID = "default"
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
module.exports = { module.exports = {
configs, configs,
users,
TENANT_ID, TENANT_ID,
CSRF_TOKEN, CSRF_TOKEN,
} }

View File

@ -0,0 +1,28 @@
export const email = "test@test.com"
export const user = (userProps: any) => {
return {
email: "test@test.com",
password: "test",
roles: {},
...userProps,
}
}
export const adminUser = (userProps: any) => {
return {
...user(userProps),
admin: {
global: true,
},
}
}
export const builderUser = (userProps: any) => {
return {
...user(userProps),
builder: {
global: true,
},
}
}