@budibase/server 2.5.6-alpha.1 → 2.5.6-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builder/assets/{index.1fe52b59.js → index.a33a6c3d.js} +235 -235
- package/builder/index.html +1 -1
- package/dist/api/controllers/user.js +1 -83
- package/dist/api/routes/user.js +0 -1
- package/dist/events/docUpdates/index.js +17 -0
- package/dist/events/docUpdates/processors.js +18 -0
- package/dist/events/docUpdates/syncUsers.js +49 -0
- package/dist/events/index.js +3 -0
- package/dist/package.json +7 -7
- package/dist/sdk/app/applications/sync.js +117 -23
- package/dist/sdk/users/utils.js +21 -4
- package/dist/startup.js +2 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utilities/global.js +17 -12
- package/package.json +8 -8
- package/src/api/controllers/row/internal.ts +9 -10
- package/src/api/controllers/row/utils.ts +2 -2
- package/src/api/controllers/user.ts +10 -96
- package/src/api/routes/tests/user.spec.js +0 -37
- package/src/api/routes/user.ts +0 -5
- package/src/events/docUpdates/index.ts +1 -0
- package/src/events/docUpdates/processors.ts +14 -0
- package/src/events/docUpdates/syncUsers.ts +35 -0
- package/src/events/index.ts +1 -0
- package/src/sdk/app/applications/sync.ts +129 -22
- package/src/sdk/app/applications/tests/sync.spec.ts +137 -0
- package/src/sdk/users/tests/utils.spec.ts +1 -32
- package/src/sdk/users/utils.ts +23 -5
- package/src/startup.ts +2 -1
- package/src/tests/utilities/TestConfiguration.ts +28 -0
- package/src/utilities/global.ts +21 -16
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
|
2
|
+
import { events, context, roles, constants } from "@budibase/backend-core"
|
|
3
|
+
import { init } from "../../../../events"
|
|
4
|
+
import { rawUserMetadata } from "../../../users/utils"
|
|
5
|
+
import EventEmitter from "events"
|
|
6
|
+
import { UserGroup, UserMetadata, UserRoles, User } from "@budibase/types"
|
|
7
|
+
|
|
8
|
+
const config = new TestConfiguration()
|
|
9
|
+
let app, group: UserGroup, groupUser: User
|
|
10
|
+
const ROLE_ID = roles.BUILTIN_ROLE_IDS.BASIC
|
|
11
|
+
|
|
12
|
+
const emitter = new EventEmitter()
|
|
13
|
+
|
|
14
|
+
function updateCb(docId: string) {
|
|
15
|
+
const isGroup = docId.startsWith(constants.DocumentType.GROUP)
|
|
16
|
+
if (isGroup) {
|
|
17
|
+
emitter.emit("update-group")
|
|
18
|
+
} else {
|
|
19
|
+
emitter.emit("update-user")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init(updateCb)
|
|
24
|
+
|
|
25
|
+
function waitForUpdate(opts: { group?: boolean }) {
|
|
26
|
+
return new Promise<void>((resolve, reject) => {
|
|
27
|
+
const timeout = setTimeout(() => {
|
|
28
|
+
reject()
|
|
29
|
+
}, 5000)
|
|
30
|
+
const event = opts?.group ? "update-group" : "update-user"
|
|
31
|
+
emitter.on(event, () => {
|
|
32
|
+
clearTimeout(timeout)
|
|
33
|
+
resolve()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
app = await config.init("syncApp")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
async function createUser(email: string, roles: UserRoles, builder?: boolean) {
|
|
43
|
+
const user = await config.createUser({
|
|
44
|
+
email,
|
|
45
|
+
roles,
|
|
46
|
+
builder: builder || false,
|
|
47
|
+
admin: false,
|
|
48
|
+
})
|
|
49
|
+
await context.doInContext(config.appId!, async () => {
|
|
50
|
+
await events.user.created(user)
|
|
51
|
+
})
|
|
52
|
+
return user
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function removeUserRole(user: User) {
|
|
56
|
+
const final = await config.globalUser({
|
|
57
|
+
...user,
|
|
58
|
+
id: user._id,
|
|
59
|
+
roles: {},
|
|
60
|
+
builder: false,
|
|
61
|
+
admin: false,
|
|
62
|
+
})
|
|
63
|
+
await context.doInContext(config.appId!, async () => {
|
|
64
|
+
await events.user.updated(final)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function createGroupAndUser(email: string) {
|
|
69
|
+
groupUser = await config.createUser({
|
|
70
|
+
email,
|
|
71
|
+
roles: {},
|
|
72
|
+
builder: false,
|
|
73
|
+
admin: false,
|
|
74
|
+
})
|
|
75
|
+
group = await config.createGroup()
|
|
76
|
+
await config.addUserToGroup(group._id!, groupUser._id!)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function removeUserFromGroup() {
|
|
80
|
+
await config.removeUserFromGroup(group._id!, groupUser._id!)
|
|
81
|
+
return context.doInContext(config.appId!, async () => {
|
|
82
|
+
await events.user.updated(groupUser)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function getUserMetadata(): Promise<UserMetadata[]> {
|
|
87
|
+
return context.doInContext(config.appId!, async () => {
|
|
88
|
+
return await rawUserMetadata()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildRoles() {
|
|
93
|
+
return { [config.prodAppId!]: ROLE_ID }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("app user/group sync", () => {
|
|
97
|
+
const groupEmail = "test2@test.com",
|
|
98
|
+
normalEmail = "test@test.com"
|
|
99
|
+
async function checkEmail(
|
|
100
|
+
email: string,
|
|
101
|
+
opts?: { group?: boolean; notFound?: boolean }
|
|
102
|
+
) {
|
|
103
|
+
await waitForUpdate(opts || {})
|
|
104
|
+
const metadata = await getUserMetadata()
|
|
105
|
+
const found = metadata.find(data => data.email === email)
|
|
106
|
+
if (opts?.notFound) {
|
|
107
|
+
expect(found).toBeUndefined()
|
|
108
|
+
} else {
|
|
109
|
+
expect(found).toBeDefined()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it("should be able to sync a new user, add then remove", async () => {
|
|
114
|
+
const user = await createUser(normalEmail, buildRoles())
|
|
115
|
+
await checkEmail(normalEmail)
|
|
116
|
+
await removeUserRole(user)
|
|
117
|
+
await checkEmail(normalEmail, { notFound: true })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should be able to sync a group", async () => {
|
|
121
|
+
await createGroupAndUser(groupEmail)
|
|
122
|
+
await checkEmail(groupEmail, { group: true })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("should be able to remove user from group", async () => {
|
|
126
|
+
if (!group) {
|
|
127
|
+
await createGroupAndUser(groupEmail)
|
|
128
|
+
}
|
|
129
|
+
await removeUserFromGroup()
|
|
130
|
+
await checkEmail(groupEmail, { notFound: true })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("should be able to handle builder users", async () => {
|
|
134
|
+
await createUser("test3@test.com", {}, true)
|
|
135
|
+
await checkEmail("test3@test.com")
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -121,38 +121,7 @@ describe("syncGlobalUsers", () => {
|
|
|
121
121
|
await syncGlobalUsers()
|
|
122
122
|
|
|
123
123
|
const metadata = await rawUserMetadata()
|
|
124
|
-
expect(metadata).toHaveLength(
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it("app users are removed when app is removed from user group", async () => {
|
|
130
|
-
await config.doInTenant(async () => {
|
|
131
|
-
const group = await proSdk.groups.save(structures.userGroups.userGroup())
|
|
132
|
-
const user1 = await config.createUser({ admin: false, builder: false })
|
|
133
|
-
const user2 = await config.createUser({ admin: false, builder: false })
|
|
134
|
-
await proSdk.groups.updateGroupApps(group.id, {
|
|
135
|
-
appsToAdd: [
|
|
136
|
-
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },
|
|
137
|
-
],
|
|
138
|
-
})
|
|
139
|
-
await proSdk.groups.addUsers(group.id, [user1._id, user2._id])
|
|
140
|
-
|
|
141
|
-
await config.doInContext(config.appId, async () => {
|
|
142
|
-
await syncGlobalUsers()
|
|
143
|
-
expect(await rawUserMetadata()).toHaveLength(3)
|
|
144
|
-
|
|
145
|
-
await proSdk.groups.removeUsers(group.id, [user1._id])
|
|
146
|
-
await syncGlobalUsers()
|
|
147
|
-
|
|
148
|
-
const metadata = await rawUserMetadata()
|
|
149
|
-
expect(metadata).toHaveLength(2)
|
|
150
|
-
|
|
151
|
-
expect(metadata).not.toContainEqual(
|
|
152
|
-
expect.objectContaining({
|
|
153
|
-
_id: db.generateUserMetadataID(user1._id),
|
|
154
|
-
})
|
|
155
|
-
)
|
|
124
|
+
expect(metadata).toHaveLength(0)
|
|
156
125
|
})
|
|
157
126
|
})
|
|
158
127
|
})
|
package/src/sdk/users/utils.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { getGlobalUsers } from "../../utilities/global"
|
|
2
2
|
import { context, roles as rolesCore } from "@budibase/backend-core"
|
|
3
3
|
import {
|
|
4
|
+
getGlobalIDFromUserMetadataID,
|
|
4
5
|
generateUserMetadataID,
|
|
5
6
|
getUserMetadataParams,
|
|
6
7
|
InternalTables,
|
|
7
8
|
} from "../../db/utils"
|
|
8
9
|
import { isEqual } from "lodash"
|
|
9
|
-
import { ContextUser, UserMetadata } from "@budibase/types"
|
|
10
|
+
import { ContextUser, UserMetadata, User } from "@budibase/types"
|
|
10
11
|
|
|
11
12
|
export function combineMetadataAndUser(
|
|
12
13
|
user: ContextUser,
|
|
@@ -37,6 +38,10 @@ export function combineMetadataAndUser(
|
|
|
37
38
|
if (found) {
|
|
38
39
|
newDoc._rev = found._rev
|
|
39
40
|
}
|
|
41
|
+
// clear fields that shouldn't be in metadata
|
|
42
|
+
delete newDoc.password
|
|
43
|
+
delete newDoc.forceResetPassword
|
|
44
|
+
delete newDoc.roles
|
|
40
45
|
if (found == null || !isEqual(newDoc, found)) {
|
|
41
46
|
return {
|
|
42
47
|
...found,
|
|
@@ -60,10 +65,9 @@ export async function rawUserMetadata() {
|
|
|
60
65
|
export async function syncGlobalUsers() {
|
|
61
66
|
// sync user metadata
|
|
62
67
|
const db = context.getAppDB()
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
])
|
|
68
|
+
const resp = await Promise.all([getGlobalUsers(), rawUserMetadata()])
|
|
69
|
+
const users = resp[0] as User[]
|
|
70
|
+
const metadata = resp[1] as UserMetadata[]
|
|
67
71
|
const toWrite = []
|
|
68
72
|
for (let user of users) {
|
|
69
73
|
const combined = combineMetadataAndUser(user, metadata)
|
|
@@ -71,5 +75,19 @@ export async function syncGlobalUsers() {
|
|
|
71
75
|
toWrite.push(combined)
|
|
72
76
|
}
|
|
73
77
|
}
|
|
78
|
+
let foundEmails: string[] = []
|
|
79
|
+
for (let data of metadata) {
|
|
80
|
+
if (!data._id) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
const alreadyExisting = data.email && foundEmails.indexOf(data.email) !== -1
|
|
84
|
+
const globalId = getGlobalIDFromUserMetadataID(data._id)
|
|
85
|
+
if (!users.find(user => user._id === globalId) || alreadyExisting) {
|
|
86
|
+
toWrite.push({ ...data, _deleted: true })
|
|
87
|
+
}
|
|
88
|
+
if (data.email) {
|
|
89
|
+
foundEmails.push(data.email)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
74
92
|
await db.bulkDocs(toWrite)
|
|
75
93
|
}
|
package/src/startup.ts
CHANGED
|
@@ -10,7 +10,7 @@ import fs from "fs"
|
|
|
10
10
|
import { watch } from "./watch"
|
|
11
11
|
import * as automations from "./automations"
|
|
12
12
|
import * as fileSystem from "./utilities/fileSystem"
|
|
13
|
-
import eventEmitter from "./events"
|
|
13
|
+
import { default as eventEmitter, init as eventInit } from "./events"
|
|
14
14
|
import * as migrations from "./migrations"
|
|
15
15
|
import * as bullboard from "./automations/bullboard"
|
|
16
16
|
import * as pro from "@budibase/pro"
|
|
@@ -63,6 +63,7 @@ export async function startup(app?: any, server?: any) {
|
|
|
63
63
|
eventEmitter.emitPort(env.PORT)
|
|
64
64
|
fileSystem.init()
|
|
65
65
|
await redis.init()
|
|
66
|
+
eventInit()
|
|
66
67
|
|
|
67
68
|
// run migrations on startup if not done via http
|
|
68
69
|
// not recommended in a clustered environment
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
SearchFilters,
|
|
50
50
|
UserRoles,
|
|
51
51
|
} from "@budibase/types"
|
|
52
|
+
import { BUILTIN_ROLE_IDS } from "@budibase/backend-core/src/security/roles"
|
|
52
53
|
|
|
53
54
|
type DefaultUserValues = {
|
|
54
55
|
globalUserId: string
|
|
@@ -306,6 +307,33 @@ class TestConfiguration {
|
|
|
306
307
|
}
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
async createGroup(roleId: string = BUILTIN_ROLE_IDS.BASIC) {
|
|
311
|
+
return context.doInTenant(this.tenantId!, async () => {
|
|
312
|
+
const baseGroup = structures.userGroups.userGroup()
|
|
313
|
+
baseGroup.roles = {
|
|
314
|
+
[this.prodAppId]: roleId,
|
|
315
|
+
}
|
|
316
|
+
const { id, rev } = await pro.sdk.groups.save(baseGroup)
|
|
317
|
+
return {
|
|
318
|
+
_id: id,
|
|
319
|
+
_rev: rev,
|
|
320
|
+
...baseGroup,
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async addUserToGroup(groupId: string, userId: string) {
|
|
326
|
+
return context.doInTenant(this.tenantId!, async () => {
|
|
327
|
+
await pro.sdk.groups.addUsers(groupId, [userId])
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async removeUserFromGroup(groupId: string, userId: string) {
|
|
332
|
+
return context.doInTenant(this.tenantId!, async () => {
|
|
333
|
+
await pro.sdk.groups.removeUsers(groupId, [userId])
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
309
337
|
async login({ roleId, userId, builder, prodApp = false }: any = {}) {
|
|
310
338
|
const appId = prodApp ? this.prodAppId : this.appId
|
|
311
339
|
return context.doInAppContext(appId, async () => {
|
package/src/utilities/global.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import env from "../environment"
|
|
10
10
|
import { groups } from "@budibase/pro"
|
|
11
11
|
import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types"
|
|
12
|
+
import { global } from "yargs"
|
|
12
13
|
|
|
13
14
|
export function updateAppRole(
|
|
14
15
|
user: ContextUser,
|
|
@@ -16,7 +17,7 @@ export function updateAppRole(
|
|
|
16
17
|
) {
|
|
17
18
|
appId = appId || context.getAppId()
|
|
18
19
|
|
|
19
|
-
if (!user || !user.roles) {
|
|
20
|
+
if (!user || (!user.roles && !user.userGroups)) {
|
|
20
21
|
return user
|
|
21
22
|
}
|
|
22
23
|
// if in an multi-tenancy environment make sure roles are never updated
|
|
@@ -27,7 +28,7 @@ export function updateAppRole(
|
|
|
27
28
|
return user
|
|
28
29
|
}
|
|
29
30
|
// always use the deployed app
|
|
30
|
-
if (appId) {
|
|
31
|
+
if (appId && user.roles) {
|
|
31
32
|
user.roleId = user.roles[dbCore.getProdAppID(appId)]
|
|
32
33
|
}
|
|
33
34
|
// if a role wasn't found then either set as admin (builder) or public (everyone else)
|
|
@@ -60,7 +61,7 @@ async function checkGroupRoles(
|
|
|
60
61
|
return user
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
async function processUser(
|
|
64
|
+
export async function processUser(
|
|
64
65
|
user: ContextUser,
|
|
65
66
|
opts: { appId?: string; groups?: UserGroup[] } = {}
|
|
66
67
|
) {
|
|
@@ -94,16 +95,15 @@ export async function getGlobalUser(userId: string) {
|
|
|
94
95
|
return processUser(user, { appId })
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
export async function getGlobalUsers(
|
|
98
|
+
export async function getGlobalUsers(
|
|
99
|
+
userIds?: string[],
|
|
100
|
+
opts?: { noProcessing?: boolean }
|
|
101
|
+
) {
|
|
98
102
|
const appId = context.getAppId()
|
|
99
103
|
const db = tenancy.getGlobalDB()
|
|
100
|
-
const allGroups = await groups.fetch()
|
|
101
104
|
let globalUsers
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
getGlobalIDFromUserMetadataID(user._id!)
|
|
105
|
-
)
|
|
106
|
-
globalUsers = (await db.allDocs(getMultiIDParams(globalIds))).rows.map(
|
|
105
|
+
if (userIds) {
|
|
106
|
+
globalUsers = (await db.allDocs(getMultiIDParams(userIds))).rows.map(
|
|
107
107
|
row => row.doc
|
|
108
108
|
)
|
|
109
109
|
} else {
|
|
@@ -126,15 +126,20 @@ export async function getGlobalUsers(users?: ContextUser[]) {
|
|
|
126
126
|
return globalUsers
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
if (opts?.noProcessing) {
|
|
130
|
+
return globalUsers
|
|
131
|
+
} else {
|
|
132
|
+
// pass in the groups, meaning we don't actually need to retrieve them for
|
|
133
|
+
// each user individually
|
|
134
|
+
const allGroups = await groups.fetch()
|
|
135
|
+
return Promise.all(
|
|
136
|
+
globalUsers.map(user => processUser(user, { groups: allGroups }))
|
|
137
|
+
)
|
|
138
|
+
}
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
export async function getGlobalUsersFromMetadata(users: ContextUser[]) {
|
|
137
|
-
const globalUsers = await getGlobalUsers(users)
|
|
142
|
+
const globalUsers = await getGlobalUsers(users.map(user => user._id!))
|
|
138
143
|
return users.map(user => {
|
|
139
144
|
const globalUser = globalUsers.find(
|
|
140
145
|
globalUser => globalUser && user._id?.includes(globalUser._id)
|