User fixes wip

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

View File

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

View File

@ -5,6 +5,8 @@ const {
SEPARATOR,
} = require("./utils")
const { getGlobalDB } = require("../tenancy")
const { StaticDatabases } = require("./constants")
const { doWithDB } = require("./");
const DESIGN_DB = "_design/database"
@ -56,6 +58,31 @@ exports.createNewUserEmailView = async () => {
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 () => {
const db = getGlobalDB()
let designDoc
@ -128,23 +155,17 @@ exports.createUserBuildersView = async () => {
await db.put(designDoc)
}
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()
}
exports.queryView = async (viewName, params, db, CreateFuncByName) => {
try {
let response = (await db.query(`database/${viewName}`, params)).rows
response = response.map(resp =>
params.include_docs ? resp.doc : resp.value
)
if (params.arrayResponse) {
return response
} else {
return response.length <= 1 ? response[0] : response
}
} catch (err) {
if (err != null && err.name === "not_found") {
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 "./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
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
userGroups?: string[]
forceResetPassword?: boolean
}
export interface UserRoles {
[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 "./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 {
let response = await users.bulkCreate(newUsersRequested, groups)
await groupUtils.bulkSaveGroupUsers(groupsToSave, response)
const response = await users.bulkCreate(newUsersRequested, groups)
await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful)
ctx.body = response
} catch (err: any) {

View File

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

View File

@ -14,8 +14,10 @@ import {
HTTPError,
accounts,
migrations,
StaticDatabases,
ViewName
} 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"
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 (
newUsersRequested: User[],
groups: string[]
@ -257,19 +307,16 @@ export const bulkCreate = async (
let usersToSave: any[] = []
let newUsers: any[] = []
const allUsers = await db.allDocs(
dbUtils.getGlobalUserParams(null, {
include_docs: true,
})
)
let mapped = allUsers.rows.map((row: any) => row.id)
const emails = newUsersRequested.map((user: User) => user.email)
const existingEmails = await searchExistingEmails(emails)
const unsuccessful: { email: string, reason: string }[] = []
const currentUserEmails = mapped.map((x: any) => x.email) || []
for (const newUser of newUsersRequested) {
if (
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
}
newUser.userGroups = groups
@ -307,12 +354,17 @@ export const bulkCreate = async (
await apps.syncUserInApps(user._id)
}
return usersToBulkSave.map(user => {
const saved = usersToBulkSave.map(user => {
return {
_id: user._id,
email: user.email,
}
})
return {
successful: saved,
unsuccessful
}
}
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")
const users = require("./users")
const groups = require("./groups")
import configs from "./configs"
import * as users from "./users"
import * as groups from "./groups"
const TENANT_ID = "default"
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
module.exports = {
export = {
configs,
users,
TENANT_ID,

View File

@ -1,6 +1,7 @@
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 {
email: "test@test.com",
password: "test",
@ -9,16 +10,19 @@ export const user = (userProps: any) => {
}
}
export const adminUser = (userProps: any) => {
export const adminUser = (userProps: any): AdminUser => {
return {
...user(userProps),
admin: {
global: true,
},
builder: {
global: true
}
}
}
export const builderUser = (userProps: any) => {
export const builderUser = (userProps: any): BuilderUser => {
return {
...user(userProps),
builder: {