User fixes wip

This commit is contained in:
Rory Powell 2022-08-23 09:37:13 +01:00
parent f2bfe40c60
commit c29f3768fa
14 changed files with 296 additions and 116 deletions

View File

@ -18,6 +18,7 @@ export enum ViewName {
LINK = "by_link", LINK = "by_link",
ROUTING = "screen_routes", ROUTING = "screen_routes",
AUTOMATION_LOGS = "automation_logs", AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email",
} }
export const DeprecatedViews = { export const DeprecatedViews = {
@ -41,6 +42,7 @@ export enum DocumentType {
MIGRATIONS = "migrations", MIGRATIONS = "migrations",
DEV_INFO = "devinfo", DEV_INFO = "devinfo",
AUTOMATION_LOG = "log_au", AUTOMATION_LOG = "log_au",
ACCOUNT = "acc",
} }
export const StaticDatabases = { export const StaticDatabases = {

View File

@ -5,6 +5,8 @@ const {
SEPARATOR, SEPARATOR,
} = require("./utils") } = require("./utils")
const { getGlobalDB } = require("../tenancy") const { getGlobalDB } = require("../tenancy")
const { StaticDatabases } = require("./constants")
const { doWithDB } = require("./");
const DESIGN_DB = "_design/database" const DESIGN_DB = "_design/database"
@ -56,6 +58,31 @@ exports.createNewUserEmailView = async () => {
await db.put(designDoc) await db.put(designDoc)
} }
exports.createAccountEmailView = async () => {
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db) => {
let designDoc
try {
designDoc = await db.get(DESIGN_DB)
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${DocumentType.ACCOUNT}${SEPARATOR}")) {
emit(doc.email.toLowerCase(), doc.tenantId)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.ACCOUNT_BY_EMAIL]: view,
}
await db.put(designDoc)
})
}
exports.createUserAppView = async () => { exports.createUserAppView = async () => {
const db = getGlobalDB() const db = getGlobalDB()
let designDoc let designDoc
@ -128,23 +155,17 @@ exports.createUserBuildersView = async () => {
await db.put(designDoc) await db.put(designDoc)
} }
exports.queryGlobalView = async (viewName, params, db = null) => { exports.queryView = async (viewName, params, db, CreateFuncByName) => {
const CreateFuncByName = {
[ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
[ViewName.BY_API_KEY]: exports.createApiKeyView,
[ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
[ViewName.USER_BY_APP]: exports.createUserAppView,
}
// can pass DB in if working with something specific
if (!db) {
db = getGlobalDB()
}
try { try {
let response = (await db.query(`database/${viewName}`, params)).rows let response = (await db.query(`database/${viewName}`, params)).rows
response = response.map(resp => response = response.map(resp =>
params.include_docs ? resp.doc : resp.value params.include_docs ? resp.doc : resp.value
) )
if (params.arrayResponse) {
return response
} else {
return response.length <= 1 ? response[0] : response return response.length <= 1 ? response[0] : response
}
} catch (err) { } catch (err) {
if (err != null && err.name === "not_found") { if (err != null && err.name === "not_found") {
const createFunc = CreateFuncByName[viewName] const createFunc = CreateFuncByName[viewName]
@ -156,3 +177,27 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
} }
} }
} }
exports.queryPlatformView = async (viewName, params) => {
const CreateFuncByName = {
[ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView,
}
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db) => {
return exports.queryView(viewName, params, db, CreateFuncByName)
})
}
exports.queryGlobalView = async (viewName, params, db = null) => {
const CreateFuncByName = {
[ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
[ViewName.BY_API_KEY]: exports.createApiKeyView,
[ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
[ViewName.USER_BY_APP]: exports.createUserAppView,
}
// can pass DB in if working with something specific
if (!db) {
db = getGlobalDB()
}
return exports.queryView(viewName, params, db, CreateFuncByName)
}

View File

@ -1 +1,2 @@
export * from "./analytics" export * from "./analytics"
export * from "./user"

View File

@ -0,0 +1,10 @@
import { User } from "../../documents"
export interface BulkCreateUsersRequest {
users: User[]
groups: any[]
}
export interface BulkDeleteUsersRequest {
userIds: string[]
}

View File

@ -15,8 +15,27 @@ export interface User extends Document {
status?: string status?: string
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now() createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
userGroups?: string[] userGroups?: string[]
forceResetPassword?: boolean
} }
export interface UserRoles { export interface UserRoles {
[key: string]: string [key: string]: string
} }
// utility types
export interface BuilderUser extends User {
builder: {
global: boolean
}
}
export interface AdminUser extends User {
admin: {
global: boolean
},
builder: {
global: boolean
}
}

View File

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

View File

@ -0,0 +1,9 @@
import { Document } from "../document";
/**
* doc id is user email
*/
export interface PlatformUserByEmail extends Document {
tenantId: string
userId: string
}

View File

@ -46,8 +46,8 @@ export const bulkCreate = async (ctx: any) => {
} }
try { try {
let response = await users.bulkCreate(newUsersRequested, groups) const response = await users.bulkCreate(newUsersRequested, groups)
await groupUtils.bulkSaveGroupUsers(groupsToSave, response) await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful)
ctx.body = response ctx.body = response
} catch (err: any) { } catch (err: any) {

View File

@ -1,7 +1,9 @@
jest.mock("nodemailer") jest.mock("nodemailer")
const { config, request, mocks, structures } = require("../../../tests") import { config, request, mocks, structures } from "../../../tests"
const sendMailMock = mocks.email.mock() const sendMailMock = mocks.email.mock()
const { events } = require("@budibase/backend-core") import { events } from "@budibase/backend-core"
import { User, BulkCreateUsersRequest, BulkDeleteUsersRequest } from "@budibase/types"
describe("/api/global/users", () => { describe("/api/global/users", () => {
beforeAll(async () => { beforeAll(async () => {
@ -12,6 +14,10 @@ describe("/api/global/users", () => {
await config.afterAll() await config.afterAll()
}) })
beforeEach(() => {
jest.clearAllMocks()
})
const sendUserInvite = async () => { const sendUserInvite = async () => {
await config.saveSmtpConfig() await config.saveSmtpConfig()
await config.saveSettingsConfig() await config.saveSettingsConfig()
@ -31,6 +37,7 @@ describe("/api/global/users", () => {
return { code, res } return { code, res }
} }
describe("invite", () => {
it("should be able to generate an invitation", async () => { it("should be able to generate an invitation", async () => {
const { code, res } = await sendUserInvite() const { code, res } = await sendUserInvite()
@ -58,8 +65,47 @@ describe("/api/global/users", () => {
expect(events.user.inviteAccepted).toBeCalledTimes(1) expect(events.user.inviteAccepted).toBeCalledTimes(1)
expect(events.user.inviteAccepted).toBeCalledWith(user) expect(events.user.inviteAccepted).toBeCalledWith(user)
}) })
})
const createUser = async (user) => { const bulkCreateUsers = async (users: User[], groups: any[] = []) => {
const body: BulkCreateUsersRequest = { users, groups }
const res = await request
.post(`/api/global/users/bulkCreate`)
.send(body)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body
}
describe("bulkCreate", () => {
it("should ignore users existing in the same tenant", async () => {
await bulkCreateUsers(toCreate)
})
it("should ignore users existing in other tenants", async () => {
await bulkCreateUsers(toCreate)
})
it("should ignore accounts using the same email", async () => {
await bulkCreateUsers(toCreate)
})
it("should be able to bulkCreate users with different permissions", async () => {
const builder = structures.users.builderUser({ email: "bulkbasic@test.com" })
const admin = structures.users.adminUser({ email: "bulkadmin@test.com" })
const user = structures.users.user({ email: "bulkuser@test.com" })
await bulkCreateUsers([builder, admin, user])
expect(events.user.created).toBeCalledTimes(3)
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
})
})
const createUser = async (user: User) => {
const existing = await config.getUser(user.email) const existing = await config.getUser(user.email)
if (existing) { if (existing) {
await deleteUser(existing._id) await deleteUser(existing._id)
@ -67,13 +113,13 @@ describe("/api/global/users", () => {
return saveUser(user) return saveUser(user)
} }
const updateUser = async (user) => { const updateUser = async (user: User) => {
const existing = await config.getUser(user.email) const existing = await config.getUser(user.email)
user._id = existing._id user._id = existing._id
return saveUser(user) return saveUser(user)
} }
const saveUser = async (user) => { const saveUser = async (user: User) => {
const res = await request const res = await request
.post(`/api/global/users`) .post(`/api/global/users`)
.send(user) .send(user)
@ -83,30 +129,20 @@ describe("/api/global/users", () => {
return res.body return res.body
} }
const bulkDeleteUsers = async (users: User[]) => {
const bulkCreateUsers = async (users) => { const body: BulkDeleteUsersRequest = {
const res = await request userIds: users.map(u => u._id!)
.post(`/api/global/users/bulkCreate`)
.send(users)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body
} }
const bulkDeleteUsers = async (users) => {
const res = await request const res = await request
.post(`/api/global/users/bulkDelete`) .post(`/api/global/users/bulkDelete`)
.send(users) .send(body)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
return res.body return res.body
} }
const deleteUser = async (email: string) => {
const deleteUser = async (email) => {
const user = await config.getUser(email) const user = await config.getUser(email)
if (user) { if (user) {
await request await request
@ -119,7 +155,6 @@ describe("/api/global/users", () => {
describe("create", () => { describe("create", () => {
it("should be able to create a basic user", async () => { it("should be able to create a basic user", async () => {
jest.clearAllMocks()
const user = structures.users.user({ email: "basic@test.com" }) const user = structures.users.user({ email: "basic@test.com" })
await createUser(user) await createUser(user)
@ -129,23 +164,8 @@ describe("/api/global/users", () => {
expect(events.user.permissionAdminAssigned).not.toBeCalled() expect(events.user.permissionAdminAssigned).not.toBeCalled()
}) })
it("should be able to bulkCreate users with different permissions", async () => {
jest.clearAllMocks()
const builder = structures.users.builderUser({ email: "bulkbasic@test.com" })
const admin = structures.users.adminUser({ email: "bulkadmin@test.com" })
const user = structures.users.user({ email: "bulkuser@test.com" })
let toCreate = { users: [builder, admin, user], groups: [] }
await bulkCreateUsers(toCreate)
expect(events.user.created).toBeCalledTimes(3)
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
})
it("should be able to create an admin user", async () => { it("should be able to create an admin user", async () => {
jest.clearAllMocks()
const user = structures.users.adminUser({ email: "admin@test.com" }) const user = structures.users.adminUser({ email: "admin@test.com" })
await createUser(user) await createUser(user)
@ -156,7 +176,6 @@ describe("/api/global/users", () => {
}) })
it("should be able to create a builder user", async () => { it("should be able to create a builder user", async () => {
jest.clearAllMocks()
const user = structures.users.builderUser({ email: "builder@test.com" }) const user = structures.users.builderUser({ email: "builder@test.com" })
await createUser(user) await createUser(user)
@ -167,7 +186,6 @@ describe("/api/global/users", () => {
}) })
it("should be able to assign app roles", async () => { it("should be able to assign app roles", async () => {
jest.clearAllMocks()
const user = structures.users.user({ email: "assign-roles@test.com" }) const user = structures.users.user({ email: "assign-roles@test.com" })
user.roles = { user.roles = {
"app_123": "role1", "app_123": "role1",
@ -230,7 +248,7 @@ describe("/api/global/users", () => {
}) })
it("should be able to update a basic user to a builder user", async () => { 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" }) const user = structures.users.user({ email: "basic-update-builder@test.com" })
await createUser(user) await createUser(user)
jest.clearAllMocks() jest.clearAllMocks()
@ -243,7 +261,7 @@ describe("/api/global/users", () => {
}) })
it("should be able to update an admin user to a basic user", async () => { 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" }) const user = structures.users.adminUser({ email: "admin-update-basic@test.com" })
await createUser(user) await createUser(user)
jest.clearAllMocks() jest.clearAllMocks()
@ -257,7 +275,7 @@ describe("/api/global/users", () => {
}) })
it("should be able to update an builder user to a basic user", async () => { 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" }) const user = structures.users.builderUser({ email: "builder-update-basic@test.com" })
await createUser(user) await createUser(user)
jest.clearAllMocks() jest.clearAllMocks()
@ -334,6 +352,29 @@ describe("/api/global/users", () => {
}) })
}) })
describe("bulkDelete", () => {
it("should not be able to bulkDelete account admin as admin", async () => {
})
it("should not be able to bulkDelete account owner as account owner", async () => {
})
it("should be able to bulk delete users with different permissions", async () => {
const builder = structures.users.builderUser({ email: "basic@test.com" })
const admin = structures.users.adminUser({ email: "admin@test.com" })
const user = structures.users.user({ email: "user@test.com" })
const createdUsers = await bulkCreateUsers([builder, admin, user])
await bulkDeleteUsers(createdUsers)
expect(events.user.deleted).toBeCalledTimes(3)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
})
})
describe("destroy", () => { describe("destroy", () => {
it("should be able to destroy a basic user", async () => { it("should be able to destroy a basic user", async () => {
let user = structures.users.user({ email: "destroy@test.com" }) let user = structures.users.user({ email: "destroy@test.com" })
@ -371,18 +412,11 @@ describe("/api/global/users", () => {
expect(events.user.permissionAdminRemoved).not.toBeCalled() expect(events.user.permissionAdminRemoved).not.toBeCalled()
}) })
it("should be able to bulk delete users with different permissions", async () => { it("should not be able to destroy account admin as admin", async () => {
jest.clearAllMocks()
const builder = structures.users.builderUser({ email: "basic@test.com" })
const admin = structures.users.adminUser({ email: "admin@test.com" })
const user = structures.users.user({ email: "user@test.com" })
let toCreate = { users: [builder, admin, user], groups: [] } })
let createdUsers = await bulkCreateUsers(toCreate)
await bulkDeleteUsers({ userIds: [createdUsers[0]._id, createdUsers[1]._id, createdUsers[2]._id] }) it("should not be able to destroy account owner as account owner", async () => {
expect(events.user.deleted).toBeCalledTimes(3)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
}) })

View File

@ -14,8 +14,10 @@ import {
HTTPError, HTTPError,
accounts, accounts,
migrations, migrations,
StaticDatabases,
ViewName
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { MigrationType, User } from "@budibase/types" import { MigrationType, PlatformUserByEmail, User, Account } from "@budibase/types"
import { groups as groupUtils } from "@budibase/pro" import { groups as groupUtils } from "@budibase/pro"
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
@ -247,6 +249,54 @@ export const addTenant = async (
} }
} }
const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
keys: emails,
include_docs: true,
arrayResponse: true
})
}
const getExistingPlatformUsers = async (emails: string[]): Promise<PlatformUserByEmail[]> => {
return dbUtils.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (infoDb: any) => {
const response = await infoDb.allDocs({
keys: emails,
include_docs: true,
})
return response.rows.map((row: any) => row.doc)
})
}
const getExistingAccounts = async (emails: string[]): Promise<Account[]> => {
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
keys: emails,
include_docs: true,
arrayResponse: true
})
}
/**
* Apply a system-wide search on emails:
* - in tenant
* - cross tenant
* - accounts
* return an array of emails that match the supplied emails.
*/
const searchExistingEmails = async (emails: string[]) => {
let matchedEmails: string[] = []
const existingTenantUsers = await getExistingTenantUsers(emails)
matchedEmails.push(...existingTenantUsers.map((user: User) => user.email))
const existingPlatformUsers = await getExistingPlatformUsers(emails)
matchedEmails.push(...existingPlatformUsers.map((user: PlatformUserByEmail) => user._id!))
const existingAccounts = await getExistingAccounts(emails)
matchedEmails.push(...existingAccounts.map((account: Account) => account.email))
return matchedEmails
}
export const bulkCreate = async ( export const bulkCreate = async (
newUsersRequested: User[], newUsersRequested: User[],
groups: string[] groups: string[]
@ -257,19 +307,16 @@ export const bulkCreate = async (
let usersToSave: any[] = [] let usersToSave: any[] = []
let newUsers: any[] = [] let newUsers: any[] = []
const allUsers = await db.allDocs( const emails = newUsersRequested.map((user: User) => user.email)
dbUtils.getGlobalUserParams(null, { const existingEmails = await searchExistingEmails(emails)
include_docs: true, const unsuccessful: { email: string, reason: string }[] = []
})
)
let mapped = allUsers.rows.map((row: any) => row.id)
const currentUserEmails = mapped.map((x: any) => x.email) || []
for (const newUser of newUsersRequested) { for (const newUser of newUsersRequested) {
if ( if (
newUsers.find((x: any) => x.email === newUser.email) || newUsers.find((x: any) => x.email === newUser.email) ||
currentUserEmails.includes(newUser.email) existingEmails.includes(newUser.email)
) { ) {
unsuccessful.push({ email: newUser.email, reason: `Email address ${newUser.email} already in use.` })
continue continue
} }
newUser.userGroups = groups newUser.userGroups = groups
@ -307,12 +354,17 @@ export const bulkCreate = async (
await apps.syncUserInApps(user._id) await apps.syncUserInApps(user._id)
} }
return usersToBulkSave.map(user => { const saved = usersToBulkSave.map(user => {
return { return {
_id: user._id, _id: user._id,
email: user.email, email: user.email,
} }
}) })
return {
successful: saved,
unsuccessful
}
} }
export const bulkDelete = async (userIds: any) => { export const bulkDelete = async (userIds: any) => {

View File

@ -1,12 +0,0 @@
const TestConfiguration = require("./TestConfiguration")
const structures = require("./structures")
const mocks = require("./mocks")
const config = new TestConfiguration()
const request = config.getRequest()
module.exports = {
structures,
mocks,
config,
request,
}

View File

@ -0,0 +1,15 @@
import TestConfiguration from "./TestConfiguration"
import structures from "./structures"
import mocks from "./mocks"
const config = new TestConfiguration()
const request = config.getRequest()
const pkg = {
structures,
mocks,
config,
request,
}
export = pkg

View File

@ -1,11 +1,11 @@
const configs = require("./configs") import configs from "./configs"
const users = require("./users") import * as users from "./users"
const groups = require("./groups") import * as groups from "./groups"
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 = { export = {
configs, configs,
users, users,
TENANT_ID, TENANT_ID,

View File

@ -1,6 +1,7 @@
export const email = "test@test.com" export const email = "test@test.com"
import { AdminUser, BuilderUser, User } from "@budibase/types"
export const user = (userProps: any) => { export const user = (userProps: any): User => {
return { return {
email: "test@test.com", email: "test@test.com",
password: "test", password: "test",
@ -9,16 +10,19 @@ export const user = (userProps: any) => {
} }
} }
export const adminUser = (userProps: any) => { export const adminUser = (userProps: any): AdminUser => {
return { return {
...user(userProps), ...user(userProps),
admin: { admin: {
global: true, global: true,
}, },
builder: {
global: true
}
} }
} }
export const builderUser = (userProps: any) => { export const builderUser = (userProps: any): BuilderUser => {
return { return {
...user(userProps), ...user(userProps),
builder: { builder: {