user / rbac events + tests
This commit is contained in:
parent
ac8573b67e
commit
e98e659346
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
|
@ -0,0 +1 @@
|
||||||
|
export * as users from "./users"
|
|
@ -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(),
|
|
@ -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 { 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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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