user / rbac events + tests
This commit is contained in:
parent
ac8573b67e
commit
e98e659346
|
@ -1,8 +1,8 @@
|
|||
class BudibaseError extends Error {
|
||||
constructor(message, type, code) {
|
||||
constructor(message, code, type) {
|
||||
super(message)
|
||||
this.type = type
|
||||
this.code = code
|
||||
this.type = type
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
const http = require("./http")
|
||||
const licensing = require("./licensing")
|
||||
|
||||
const codes = {
|
||||
...licensing.codes,
|
||||
}
|
||||
|
||||
const types = {
|
||||
...licensing.types,
|
||||
}
|
||||
const types = [licensing.type]
|
||||
|
||||
const context = {
|
||||
...licensing.context,
|
||||
|
@ -36,6 +35,9 @@ const getPublicError = err => {
|
|||
module.exports = {
|
||||
codes,
|
||||
types,
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
errors: {
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
HTTPError: http.HTTPError,
|
||||
},
|
||||
getPublicError,
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
const { BudibaseError } = require("./base")
|
||||
const { HTTPError } = require("./http")
|
||||
|
||||
const types = {
|
||||
LICENSE_ERROR: "license_error",
|
||||
}
|
||||
const type = "license_error"
|
||||
|
||||
const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
|
@ -16,16 +14,15 @@ const context = {
|
|||
},
|
||||
}
|
||||
|
||||
class UsageLimitError extends BudibaseError {
|
||||
class UsageLimitError extends HTTPError {
|
||||
constructor(message, limitName) {
|
||||
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED)
|
||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
||||
this.limitName = limitName
|
||||
this.status = 400
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
types,
|
||||
type,
|
||||
codes,
|
||||
context,
|
||||
UsageLimitError,
|
||||
|
|
|
@ -59,8 +59,10 @@ exports.Events = {
|
|||
|
||||
// ROLE
|
||||
ROLE_CREATED: "role:created",
|
||||
ROLE_UPDATED: "role:updated",
|
||||
ROLE_DELETED: "role:deleted",
|
||||
ROLE_ASSIGNED: "role:assigned",
|
||||
ROLE_UNASSIGNED: "role:unassigned",
|
||||
|
||||
// APP / CLIENT
|
||||
CLIENT_SERVED: "client:served",
|
||||
|
|
|
@ -4,7 +4,7 @@ const analytics = require("../analytics")
|
|||
const logEvent = messsage => {
|
||||
const tenantId = getTenantId()
|
||||
const userId = getTenantId() // TODO
|
||||
console.log(`[tenant=${tenantId}] [user=${userId}] ${messsage}`)
|
||||
console.log(`[audit] [tenant=${tenantId}] [user=${userId}] ${messsage}`)
|
||||
}
|
||||
|
||||
exports.processEvent = (event, properties) => {
|
||||
|
|
|
@ -8,6 +8,7 @@ const license = require("./license")
|
|||
const layout = require("./layout")
|
||||
const org = require("./org")
|
||||
const query = require("./query")
|
||||
const role = require("./role")
|
||||
const row = require("./screen")
|
||||
const table = require("./table")
|
||||
const serve = require("./serve")
|
||||
|
@ -25,6 +26,7 @@ module.exports = {
|
|||
layout,
|
||||
org,
|
||||
query,
|
||||
role,
|
||||
row,
|
||||
table,
|
||||
serve,
|
||||
|
|
|
@ -1,26 +1,31 @@
|
|||
const events = require("../events")
|
||||
const { Events } = require("../constants")
|
||||
|
||||
// TODO
|
||||
exports.updgraded = () => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.LICENSE_UPGRADED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.downgraded = () => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.LICENSE_DOWNGRADED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.updated = () => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.LICENSE_UPDATED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.activated = () => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.LICENSE_ACTIVATED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.quotaExceeded = (quotaName, value) => {
|
||||
const properties = {
|
||||
name: quotaName,
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
const events = require("../events")
|
||||
const { Events } = require("../constants")
|
||||
|
||||
exports.created = () => {
|
||||
/* eslint-disable */
|
||||
|
||||
exports.created = (datasource, query) => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.QUERY_CREATED, properties)
|
||||
}
|
||||
|
||||
exports.updated = () => {
|
||||
exports.updated = (datasource, query) => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.QUERY_UPDATED, properties)
|
||||
}
|
||||
|
||||
exports.deleted = () => {
|
||||
exports.deleted = (datasource, query) => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.QUERY_DELETED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.import = () => {
|
||||
exports.import = (datasource, importSource, count) => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.QUERY_IMPORT, properties)
|
||||
}
|
||||
|
@ -28,8 +29,7 @@ exports.import = () => {
|
|||
// events.processEvent(Events.QUERY_RUN, properties)
|
||||
// }
|
||||
|
||||
// TODO
|
||||
exports.previewed = () => {
|
||||
exports.previewed = datasource => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.QUERY_PREVIEWED, properties)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
const events = require("../events")
|
||||
const { Events } = require("../constants")
|
||||
|
||||
exports.created = () => {
|
||||
/* eslint-disable */
|
||||
|
||||
exports.created = role => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.ROLE_CREATED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.deleted = () => {
|
||||
exports.updated = role => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.ROLE_UPDATED, properties)
|
||||
}
|
||||
|
||||
exports.deleted = role => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.ROLE_DELETED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.assigned = () => {
|
||||
exports.assigned = (user, role) => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.ROLE_ASSIGNED, properties)
|
||||
}
|
||||
|
||||
exports.unassigned = (user, role) => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.ROLE_UNASSIGNED, properties)
|
||||
}
|
||||
|
|
|
@ -1,85 +1,87 @@
|
|||
const events = require("../events")
|
||||
const { Events } = require("../constants")
|
||||
|
||||
// TODO
|
||||
exports.created = () => {
|
||||
/* eslint-disable */
|
||||
|
||||
exports.created = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_CREATED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.updated = () => {
|
||||
exports.updated = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_UPDATED, properties)
|
||||
}
|
||||
|
||||
exports.deleted = () => {
|
||||
exports.deleted = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_DELETED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.passwordForceReset = () => {
|
||||
exports.passwordForceReset = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PASSWORD_FORCE_RESET, properties)
|
||||
}
|
||||
|
||||
// PERMISSIONS
|
||||
|
||||
// TODO
|
||||
exports.permissionAdminAssigned = () => {
|
||||
exports.permissionAdminAssigned = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PERMISSION_ADMIN_ASSIGNED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.permissionAdminRemoved = () => {
|
||||
exports.permissionAdminRemoved = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PERMISSION_ADMIN_REMOVED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.permissionBuilderAssigned = () => {
|
||||
exports.permissionBuilderAssigned = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PERMISSION_BUILDER_ASSIGNED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.permissionBuilderRemoved = () => {
|
||||
exports.permissionBuilderRemoved = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PERMISSION_BUILDER_REMOVED, properties)
|
||||
}
|
||||
|
||||
// INVITE
|
||||
|
||||
exports.invited = () => {
|
||||
// TODO
|
||||
exports.invited = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_INVITED, properties)
|
||||
}
|
||||
|
||||
exports.inviteAccepted = () => {
|
||||
// TODO
|
||||
exports.inviteAccepted = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_INVITED_ACCEPTED, properties)
|
||||
}
|
||||
|
||||
// SELF
|
||||
|
||||
exports.selfUpdated = () => {
|
||||
// TODO
|
||||
exports.selfUpdated = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_SELF_UPDATED, properties)
|
||||
}
|
||||
|
||||
exports.selfPasswordUpdated = () => {
|
||||
// TODO
|
||||
exports.selfPasswordUpdated = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_SELF_PASSWORD_UPDATED, properties)
|
||||
}
|
||||
|
||||
exports.passwordResetRequested = () => {
|
||||
// TODO
|
||||
exports.passwordResetRequested = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PASSWORD_RESET_REQUESTED, properties)
|
||||
}
|
||||
|
||||
exports.passwordReset = () => {
|
||||
// TODO
|
||||
exports.passwordReset = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PASSWORD_RESET, properties)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const db = require("./db")
|
||||
const errors = require("./errors")
|
||||
|
||||
module.exports = {
|
||||
init(opts = {}) {
|
||||
|
@ -11,15 +12,19 @@ module.exports = {
|
|||
redis: require("../redis"),
|
||||
objectStore: require("../objectStore"),
|
||||
utils: require("../utils"),
|
||||
users: require("./users"),
|
||||
cache: require("../cache"),
|
||||
auth: require("../auth"),
|
||||
constants: require("../constants"),
|
||||
migrations: require("../migrations"),
|
||||
errors: require("./errors"),
|
||||
...errors.errors,
|
||||
env: require("./environment"),
|
||||
accounts: require("./cloud/accounts"),
|
||||
tenancy: require("./tenancy"),
|
||||
featureFlags: require("./featureFlags"),
|
||||
events: require("./events"),
|
||||
analytics: require("./analytics"),
|
||||
sessions: require("./security/sessions"),
|
||||
deprovisioning: require("./context/deprovision"),
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ const jwt = require("jsonwebtoken")
|
|||
const { UserStatus } = require("../../constants")
|
||||
const { compare } = require("../../hashing")
|
||||
const env = require("../../environment")
|
||||
const { getGlobalUserByEmail } = require("../../utils")
|
||||
const users = require("../../users")
|
||||
const { authError } = require("./utils")
|
||||
const { newid } = require("../../hashing")
|
||||
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 (!password) return authError(done, "Password Required")
|
||||
|
||||
const dbUser = await getGlobalUserByEmail(email)
|
||||
const dbUser = await users.getGlobalUserByEmail(email)
|
||||
if (dbUser == null) {
|
||||
return authError(done, "User not found")
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ const { generateGlobalUserID } = require("../../db/utils")
|
|||
const { authError } = require("./utils")
|
||||
const { newid } = require("../../hashing")
|
||||
const { createASession } = require("../../security/sessions")
|
||||
const { getGlobalUserByEmail } = require("../../utils")
|
||||
const users = require("../../users")
|
||||
const { getGlobalDB, getTenantId } = require("../../tenancy")
|
||||
const fetch = require("node-fetch")
|
||||
|
||||
|
@ -52,7 +52,7 @@ exports.authenticateThirdParty = async function (
|
|||
|
||||
// fallback to loading by email
|
||||
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
|
||||
|
@ -81,7 +81,7 @@ exports.authenticateThirdParty = async function (
|
|||
// create or sync the user
|
||||
let response
|
||||
try {
|
||||
response = await saveUserFn(dbUser, getTenantId(), false, false)
|
||||
response = await saveUserFn(dbUser, false, false)
|
||||
} catch (err) {
|
||||
return authError(done, err)
|
||||
}
|
||||
|
|
|
@ -62,6 +62,29 @@ jest.mock("../../../events", () => {
|
|||
import: 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(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -1,24 +1,10 @@
|
|||
const {
|
||||
DocumentTypes,
|
||||
SEPARATOR,
|
||||
ViewNames,
|
||||
generateGlobalUserID,
|
||||
} = require("./db/utils")
|
||||
const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const { options } = require("./middleware/passport/jwt")
|
||||
const { queryGlobalView } = require("./db/views")
|
||||
const { Headers, UserStatus, 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 { Headers, Cookies, MAX_VALID_DATE } = require("./constants")
|
||||
const env = require("./environment")
|
||||
const userCache = require("./cache/user")
|
||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||
const events = require("./events")
|
||||
|
||||
|
@ -106,8 +92,8 @@ exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
|
|||
overwrite: true,
|
||||
}
|
||||
|
||||
if (environment.COOKIE_DOMAIN) {
|
||||
config.domain = environment.COOKIE_DOMAIN
|
||||
if (env.COOKIE_DOMAIN) {
|
||||
config.domain = env.COOKIE_DOMAIN
|
||||
}
|
||||
|
||||
ctx.cookies.set(name, value, config)
|
||||
|
@ -130,23 +116,6 @@ exports.isClient = ctx => {
|
|||
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 () => {
|
||||
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
|
||||
include_docs: false,
|
||||
|
@ -154,96 +123,6 @@ exports.getBuildersCount = async () => {
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -84,7 +84,7 @@ export class RestImporter {
|
|||
const count = successQueries.length
|
||||
const importSource = this.source.getImportSource()
|
||||
const datasource = await db.get(datasourceId)
|
||||
events.query.import({ datasource, importSource, count })
|
||||
events.query.import(datasource, importSource, count)
|
||||
for (let query of successQueries) {
|
||||
events.query.created(query)
|
||||
}
|
||||
|
|
|
@ -109,8 +109,7 @@ describe("Rest Importer", () => {
|
|||
expect(importResult.errorQueries.length).toBe(0)
|
||||
expect(importResult.queries.length).toBe(assertions[key].count)
|
||||
expect(events.query.import).toBeCalledTimes(1)
|
||||
const eventData = { datasource, importSource: assertions[key].source, count: assertions[key].count}
|
||||
expect(events.query.import).toBeCalledWith(eventData)
|
||||
expect(events.query.import).toBeCalledWith(datasource, assertions[key].source, assertions[key].count)
|
||||
jest.clearAllMocks()
|
||||
}
|
||||
|
||||
|
|
|
@ -216,9 +216,12 @@ const removeDynamicVariables = async (queryId: any) => {
|
|||
|
||||
export async function destroy(ctx: any) {
|
||||
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)
|
||||
ctx.message = `Query deleted.`
|
||||
ctx.status = 200
|
||||
events.query.deleted()
|
||||
events.query.deleted(datasource, query)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ const {
|
|||
InternalTables,
|
||||
} = require("../../db/utils")
|
||||
const { getAppDB } = require("@budibase/backend-core/context")
|
||||
const { events } = require("@budibase/backend-core")
|
||||
|
||||
const UpdateRolesOptions = {
|
||||
CREATED: "created",
|
||||
|
@ -50,8 +51,10 @@ exports.find = async function (ctx) {
|
|||
exports.save = async function (ctx) {
|
||||
const db = getAppDB()
|
||||
let { _id, name, inherits, permissionId } = ctx.request.body
|
||||
let isCreate = false
|
||||
if (!_id) {
|
||||
_id = generateRoleID()
|
||||
isCreate = true
|
||||
} else if (isBuiltin(_id)) {
|
||||
ctx.throw(400, "Cannot update builtin roles.")
|
||||
}
|
||||
|
@ -62,6 +65,11 @@ exports.save = async function (ctx) {
|
|||
role._rev = ctx.request.body._rev
|
||||
}
|
||||
const result = await db.put(role)
|
||||
if (isCreate) {
|
||||
events.role.created(role)
|
||||
} else {
|
||||
events.role.updated(role)
|
||||
}
|
||||
await updateRolesOnUserTable(db, _id, UpdateRolesOptions.CREATED)
|
||||
role._rev = result.rev
|
||||
ctx.body = role
|
||||
|
@ -71,6 +79,7 @@ exports.save = async function (ctx) {
|
|||
exports.destroy = async function (ctx) {
|
||||
const db = getAppDB()
|
||||
const roleId = ctx.params.roleId
|
||||
const role = await db.get(roleId)
|
||||
if (isBuiltin(roleId)) {
|
||||
ctx.throw(400, "Cannot delete builtin role.")
|
||||
}
|
||||
|
@ -88,6 +97,7 @@ exports.destroy = async function (ctx) {
|
|||
}
|
||||
|
||||
await db.remove(roleId, ctx.params.rev)
|
||||
events.role.deleted(role)
|
||||
await updateRolesOnUserTable(
|
||||
db,
|
||||
ctx.params.roleId,
|
||||
|
|
|
@ -189,6 +189,7 @@ describe("/queries", () => {
|
|||
|
||||
expect(res.body).toEqual([])
|
||||
expect(events.query.deleted).toBeCalledTimes(1)
|
||||
expect(events.query.deleted).toBeCalledWith(datasource, query)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
|
|
|
@ -4,6 +4,7 @@ const {
|
|||
} = require("@budibase/backend-core/permissions")
|
||||
const setup = require("./utilities")
|
||||
const { basicRole } = setup.structures
|
||||
const { events } = require("@budibase/backend-core")
|
||||
|
||||
describe("/roles", () => {
|
||||
let request = setup.getRequest()
|
||||
|
@ -15,20 +16,48 @@ describe("/roles", () => {
|
|||
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", () => {
|
||||
it("returns a success message when role is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/roles`)
|
||||
.send(basicRole())
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
const res = await createRole()
|
||||
|
||||
expect(res.res.statusMessage).toEqual(
|
||||
"Role 'NewRole' created successfully."
|
||||
)
|
||||
expect(res.body._id).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 () => {
|
||||
const customRole = await config.createRole({
|
||||
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
|
||||
.delete(`/api/roles/${customRole._id}/${customRole._rev}`)
|
||||
.set(config.defaultHeaders())
|
||||
|
@ -90,6 +121,8 @@ describe("/roles", () => {
|
|||
.get(`/api/roles/${customRole._id}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect(404)
|
||||
expect(events.role.deleted).toBeCalledTimes(1)
|
||||
expect(events.role.deleted).toBeCalledWith(customRole)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,14 +4,7 @@ const { google } = require("@budibase/backend-core/middleware")
|
|||
const { oidc } = require("@budibase/backend-core/middleware")
|
||||
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||
const {
|
||||
setCookie,
|
||||
getCookie,
|
||||
clearCookie,
|
||||
getGlobalUserByEmail,
|
||||
hash,
|
||||
platformLogout,
|
||||
} = core.utils
|
||||
const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils
|
||||
const { Cookies, Headers } = core.constants
|
||||
const { passport } = core.auth
|
||||
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
||||
|
@ -21,8 +14,8 @@ const {
|
|||
isMultiTenant,
|
||||
} = require("@budibase/backend-core/tenancy")
|
||||
const env = require("../../../environment")
|
||||
import { users } from "@budibase/pro"
|
||||
const { events } = require("@budibase/backend-core")
|
||||
const { events, users: usersCore } = require("@budibase/backend-core")
|
||||
import { users } from "../../../sdk"
|
||||
|
||||
const ssoCallbackUrl = async (config: any, type: any) => {
|
||||
// incase there is a callback URL from before
|
||||
|
@ -112,7 +105,7 @@ export const reset = async (ctx: any) => {
|
|||
)
|
||||
}
|
||||
try {
|
||||
const user = await getGlobalUserByEmail(email)
|
||||
const user = await usersCore.getGlobalUserByEmail(email)
|
||||
// only if user exists, don't error though if they don't
|
||||
if (user) {
|
||||
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
|
||||
|
|
|
@ -7,7 +7,7 @@ const {
|
|||
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
||||
const { user: userCache } = require("@budibase/backend-core/cache")
|
||||
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
|
||||
const { allUsers } = require("../../utilities")
|
||||
const { users } = require("../../../sdk")
|
||||
|
||||
exports.fetch = async ctx => {
|
||||
const tenantId = ctx.user.tenantId
|
||||
|
@ -49,10 +49,10 @@ exports.find = async ctx => {
|
|||
exports.removeAppRole = async ctx => {
|
||||
const { appId } = ctx.params
|
||||
const db = getGlobalDB()
|
||||
const users = await allUsers(ctx)
|
||||
const allUsers = await users.allUsers(ctx)
|
||||
const bulk = []
|
||||
const cacheInvalidations = []
|
||||
for (let user of users) {
|
||||
for (let user of allUsers) {
|
||||
if (user.roles[appId]) {
|
||||
cacheInvalidations.push(userCache.invalidateUser(user._id))
|
||||
delete user.roles[appId]
|
||||
|
|
|
@ -13,7 +13,7 @@ const {
|
|||
} = require("@budibase/backend-core/utils")
|
||||
const { encrypt } = require("@budibase/backend-core/encryption")
|
||||
const { newid } = require("@budibase/backend-core/utils")
|
||||
const { getUser } = require("../../utilities")
|
||||
const { users } = require("../../../sdk")
|
||||
const { Cookies } = require("@budibase/backend-core/constants")
|
||||
|
||||
function newApiKey() {
|
||||
|
@ -103,7 +103,7 @@ exports.getSelf = async ctx => {
|
|||
checkCurrentApp(ctx)
|
||||
|
||||
// get the main body of the user
|
||||
ctx.body = await getUser(userId)
|
||||
ctx.body = await users.getUser(userId)
|
||||
addSessionAttributesToUser(ctx)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,18 @@
|
|||
const {
|
||||
getGlobalUserParams,
|
||||
StaticDatabases,
|
||||
} = require("@budibase/backend-core/db")
|
||||
const { getGlobalUserByEmail } = require("@budibase/backend-core/utils")
|
||||
import { EmailTemplatePurpose } from "../../../constants"
|
||||
import { checkInviteCode } from "../../../utilities/redis"
|
||||
import { sendEmail } from "../../../utilities/email"
|
||||
const { user: userCache } = require("@budibase/backend-core/cache")
|
||||
const { invalidateSessions } = require("@budibase/backend-core/sessions")
|
||||
const accounts = require("@budibase/backend-core/accounts")
|
||||
import { users } from "../../../sdk"
|
||||
|
||||
const {
|
||||
getGlobalDB,
|
||||
getTenantId,
|
||||
getTenantUser,
|
||||
doesTenantExist,
|
||||
} = require("@budibase/backend-core/tenancy")
|
||||
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"
|
||||
errors,
|
||||
users: usersCore,
|
||||
tenancy,
|
||||
db: dbUtils,
|
||||
} = require("@budibase/backend-core")
|
||||
|
||||
export const save = async (ctx: any) => {
|
||||
try {
|
||||
const user: any = await users.save(ctx.request.body, getTenantId())
|
||||
// let server know to sync user
|
||||
await syncUserInApps(user._id)
|
||||
const user = await users.save(ctx.request.body)
|
||||
ctx.body = user
|
||||
} catch (err: any) {
|
||||
ctx.throw(err.status || 400, err)
|
||||
|
@ -45,36 +31,19 @@ export const adminUser = async (ctx: any) => {
|
|||
// account portal sends no password for SSO users
|
||||
const requirePassword = parseBooleanParam(ctx.request.query.requirePassword)
|
||||
|
||||
if (await doesTenantExist(tenantId)) {
|
||||
if (await tenancy.doesTenantExist(tenantId)) {
|
||||
ctx.throw(403, "Organisation already exists.")
|
||||
}
|
||||
|
||||
const db = getGlobalDB(tenantId)
|
||||
const db = tenancy.getGlobalDB(tenantId)
|
||||
const response = await db.allDocs(
|
||||
getGlobalUserParams(null, {
|
||||
dbUtils.getGlobalUserParams(null, {
|
||||
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)) {
|
||||
ctx.throw(
|
||||
403,
|
||||
"You cannot initialise once an global user has been created."
|
||||
)
|
||||
ctx.throw(403, "You cannot initialise once a global user has been created.")
|
||||
}
|
||||
|
||||
const user = {
|
||||
|
@ -91,44 +60,25 @@ export const adminUser = async (ctx: any) => {
|
|||
tenantId,
|
||||
}
|
||||
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) {
|
||||
ctx.throw(err.status || 400, err)
|
||||
}
|
||||
}
|
||||
|
||||
export const destroy = async (ctx: any) => {
|
||||
const db = getGlobalDB()
|
||||
const dbUser = await db.get(ctx.params.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 === 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)
|
||||
const id = ctx.params.id
|
||||
await users.destroy(id, ctx.user)
|
||||
ctx.body = {
|
||||
message: `User ${ctx.params.id} deleted.`,
|
||||
message: `User ${id} deleted.`,
|
||||
}
|
||||
}
|
||||
|
||||
// called internally by app server user fetch
|
||||
export const fetch = async (ctx: any) => {
|
||||
const all = await allUsers()
|
||||
const all = await users.allUsers()
|
||||
// user hashed password shouldn't ever be returned
|
||||
for (let user of all) {
|
||||
if (user) {
|
||||
|
@ -140,12 +90,12 @@ export const fetch = async (ctx: any) => {
|
|||
|
||||
// called internally by app server user find
|
||||
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) => {
|
||||
const id = ctx.params.id
|
||||
const user = await getTenantUser(id)
|
||||
const user = await tenancy.getTenantUser(id)
|
||||
if (user) {
|
||||
ctx.body = user
|
||||
} else {
|
||||
|
@ -155,14 +105,14 @@ export const tenantUserLookup = async (ctx: any) => {
|
|||
|
||||
export const invite = async (ctx: any) => {
|
||||
let { email, userInfo } = ctx.request.body
|
||||
const existing = await getGlobalUserByEmail(email)
|
||||
const existing = await usersCore.getGlobalUserByEmail(email)
|
||||
if (existing) {
|
||||
ctx.throw(400, "Email address already in use.")
|
||||
}
|
||||
if (!userInfo) {
|
||||
userInfo = {}
|
||||
}
|
||||
userInfo.tenantId = getTenantId()
|
||||
userInfo.tenantId = tenancy.getTenantId()
|
||||
const opts: any = {
|
||||
subject: "{{ company }} platform invitation",
|
||||
info: userInfo,
|
||||
|
@ -178,16 +128,15 @@ export const inviteAccept = async (ctx: any) => {
|
|||
try {
|
||||
// info is an extension of the user object that was stored by global
|
||||
const { email, info }: any = await checkInviteCode(inviteCode)
|
||||
ctx.body = await users.save(
|
||||
{
|
||||
ctx.body = await tenancy.doInTenant(info.tenantId, () => {
|
||||
return users.save({
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
email,
|
||||
...info,
|
||||
},
|
||||
info.tenantId
|
||||
)
|
||||
})
|
||||
})
|
||||
} catch (err: any) {
|
||||
if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
|
||||
// explicitly re-throw limit exceeded errors
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../../controllers/global/self")
|
||||
const builderOnly = require("../../../middleware/builderOnly")
|
||||
const { buildUserSaveValidation } = require("../../utilities/validation")
|
||||
const { users } = require("../validation")
|
||||
|
||||
const router = Router()
|
||||
|
||||
|
@ -11,7 +11,7 @@ router
|
|||
.get("/api/global/self", controller.getSelf)
|
||||
.post(
|
||||
"/api/global/self",
|
||||
buildUserSaveValidation(true),
|
||||
users.buildUserSaveValidation(true),
|
||||
controller.updateSelf
|
||||
)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const joiValidator = require("../../../middleware/joi-validator")
|
|||
const adminOnly = require("../../../middleware/adminOnly")
|
||||
const Joi = require("joi")
|
||||
const cloudRestricted = require("../../../middleware/cloudRestricted")
|
||||
const { buildUserSaveValidation } = require("../../utilities/validation")
|
||||
const { users } = require("../validation")
|
||||
const selfController = require("../../controllers/global/self")
|
||||
|
||||
const router = Router()
|
||||
|
@ -41,7 +41,7 @@ router
|
|||
.post(
|
||||
"/api/global/users",
|
||||
adminOnly,
|
||||
buildUserSaveValidation(),
|
||||
users.buildUserSaveValidation(),
|
||||
controller.save
|
||||
)
|
||||
.get("/api/global/users", adminOnly, controller.fetch)
|
||||
|
@ -72,7 +72,7 @@ router
|
|||
.get("/api/global/users/self", selfController.getSelf)
|
||||
.post(
|
||||
"/api/global/users/self",
|
||||
buildUserSaveValidation(true),
|
||||
users.buildUserSaveValidation(true),
|
||||
selfController.updateSelf
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
jest.mock("nodemailer")
|
||||
const { config, request, mocks } = require("../../../tests")
|
||||
const { config, request, mocks, structures } = require("../../../tests")
|
||||
const sendMailMock = mocks.email.mock()
|
||||
const { events } = require("@budibase/backend-core")
|
||||
|
||||
describe("/api/global/users", () => {
|
||||
let code
|
||||
|
@ -48,4 +49,258 @@ describe("/api/global/users", () => {
|
|||
expect(user).toBeDefined()
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export * as users from "./users"
|
|
@ -1,8 +1,8 @@
|
|||
const joiValidator = require("../../middleware/joi-validator")
|
||||
const Joi = require("joi")
|
||||
import joiValidator from "../../../middleware/joi-validator"
|
||||
import Joi from "joi"
|
||||
|
||||
exports.buildUserSaveValidation = (isSelf = false) => {
|
||||
let schema = {
|
||||
export const buildUserSaveValidation = (isSelf = false) => {
|
||||
let schema: any = {
|
||||
email: Joi.string().allow(null, ""),
|
||||
password: Joi.string().allow(null, ""),
|
||||
forceResetPassword: Joi.boolean().optional(),
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * as users from "./users"
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./users"
|
|
@ -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)
|
||||
}
|
|
@ -6,7 +6,7 @@ const supertest = require("supertest")
|
|||
const { jwt } = require("@budibase/backend-core/auth")
|
||||
const { Cookies, Headers } = require("@budibase/backend-core/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 { TENANT_ID, CSRF_TOKEN } = require("./structures")
|
||||
const structures = require("./structures")
|
||||
|
@ -112,20 +112,17 @@ class TestConfiguration {
|
|||
|
||||
async getUser(email) {
|
||||
return doInTenant(TENANT_ID, () => {
|
||||
return getGlobalUserByEmail(email)
|
||||
return users.getGlobalUserByEmail(email)
|
||||
})
|
||||
}
|
||||
|
||||
async createUser(email = "test@test.com", password = "test") {
|
||||
const user = await this.getUser(email)
|
||||
async createUser(email, password) {
|
||||
const user = await this.getUser(structures.users.email)
|
||||
if (user) {
|
||||
return user
|
||||
}
|
||||
await this._req(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
structures.users.user({ email, password }),
|
||||
null,
|
||||
controllers.users.save
|
||||
)
|
||||
|
@ -133,11 +130,7 @@ class TestConfiguration {
|
|||
|
||||
async saveAdminUser() {
|
||||
await this._req(
|
||||
{
|
||||
email: "testuser@test.com",
|
||||
password: "test@test.com",
|
||||
tenantId: TENANT_ID,
|
||||
},
|
||||
structures.users.user({ tenantId: TENANT_ID }),
|
||||
null,
|
||||
controllers.users.adminUser
|
||||
)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
const configs = require("./configs")
|
||||
const users = require("./users")
|
||||
|
||||
const TENANT_ID = "default"
|
||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||
|
||||
module.exports = {
|
||||
configs,
|
||||
users,
|
||||
TENANT_ID,
|
||||
CSRF_TOKEN,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue