@budibase/worker 3.18.14 → 3.19.0
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/package.json +2 -2
- package/src/api/controllers/global/users.ts +123 -45
- package/src/api/routes/global/tests/groups.spec.ts +89 -1
- package/src/api/routes/global/tests/users.spec.ts +357 -6
- package/src/api/routes/global/users.ts +20 -18
- package/src/api/routes/index.ts +9 -10
- package/src/index.ts +20 -15
- package/src/tests/TestConfiguration.ts +16 -1
- package/src/tests/api/users.ts +44 -21
- package/src/api/routes/global/tests/appBuilder.spec.ts +0 -64
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.19.0",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -109,5 +109,5 @@
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
|
-
"gitHead": "
|
|
112
|
+
"gitHead": "72863c719173848d3128cbef9556a0af09708c20"
|
|
113
113
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
utils as backendCoreUtils,
|
|
3
|
+
cache,
|
|
4
|
+
context,
|
|
5
|
+
db,
|
|
6
|
+
events,
|
|
7
|
+
HTTPError,
|
|
8
|
+
locks,
|
|
9
|
+
platform,
|
|
10
|
+
tenancy,
|
|
11
|
+
users,
|
|
12
|
+
} from "@budibase/backend-core"
|
|
13
|
+
import { features } from "@budibase/pro"
|
|
14
|
+
import { BpmStatusKey, BpmStatusValue, utils } from "@budibase/shared-core"
|
|
3
15
|
import {
|
|
4
16
|
AcceptUserInviteRequest,
|
|
5
17
|
AcceptUserInviteResponse,
|
|
@@ -17,6 +29,7 @@ import {
|
|
|
17
29
|
DeleteInviteUsersRequest,
|
|
18
30
|
DeleteInviteUsersResponse,
|
|
19
31
|
DeleteUserResponse,
|
|
32
|
+
EditUserPermissionsResponse,
|
|
20
33
|
ErrorCode,
|
|
21
34
|
FetchUsersResponse,
|
|
22
35
|
FindUserResponse,
|
|
@@ -36,27 +49,17 @@ import {
|
|
|
36
49
|
SearchUsersResponse,
|
|
37
50
|
StrippedUser,
|
|
38
51
|
UnsavedUser,
|
|
39
|
-
UpdateInviteRequest,
|
|
40
52
|
UpdateInviteResponse,
|
|
41
53
|
User,
|
|
42
54
|
UserCtx,
|
|
43
55
|
UserIdentifier,
|
|
44
56
|
} from "@budibase/types"
|
|
45
|
-
import {
|
|
46
|
-
users,
|
|
47
|
-
cache,
|
|
48
|
-
events,
|
|
49
|
-
platform,
|
|
50
|
-
tenancy,
|
|
51
|
-
db,
|
|
52
|
-
locks,
|
|
53
|
-
context,
|
|
54
|
-
} from "@budibase/backend-core"
|
|
55
|
-
import { checkAnyUserExists } from "../../../utilities/users"
|
|
56
|
-
import { isEmailConfigured } from "../../../utilities/email"
|
|
57
|
-
import { BpmStatusKey, BpmStatusValue, utils } from "@budibase/shared-core"
|
|
58
|
-
import emailValidator from "email-validator"
|
|
59
57
|
import crypto from "crypto"
|
|
58
|
+
import emailValidator from "email-validator"
|
|
59
|
+
import env from "../../../environment"
|
|
60
|
+
import * as userSdk from "../../../sdk/users"
|
|
61
|
+
import { isEmailConfigured } from "../../../utilities/email"
|
|
62
|
+
import { checkAnyUserExists } from "../../../utilities/users"
|
|
60
63
|
|
|
61
64
|
const MAX_USERS_UPLOAD_LIMIT = 1000
|
|
62
65
|
|
|
@@ -505,45 +508,50 @@ export const getUserInvites = async (
|
|
|
505
508
|
}
|
|
506
509
|
}
|
|
507
510
|
|
|
508
|
-
export const
|
|
509
|
-
ctx: UserCtx<
|
|
511
|
+
export const addWorkspaceIdToInvite = async (
|
|
512
|
+
ctx: UserCtx<void, UpdateInviteResponse, { code: string; role: string }>
|
|
510
513
|
) => {
|
|
511
|
-
const { code } = ctx.params
|
|
512
|
-
let updateBody = { ...ctx.request.body }
|
|
514
|
+
const { code, role } = ctx.params
|
|
513
515
|
|
|
514
|
-
|
|
516
|
+
const workspaceId = await backendCoreUtils.getAppIdFromCtx(ctx)
|
|
517
|
+
if (!workspaceId) {
|
|
518
|
+
ctx.throw(400, "Workspace id not set")
|
|
519
|
+
}
|
|
520
|
+
const prodWorkspaceId = db.getProdWorkspaceID(workspaceId)
|
|
515
521
|
|
|
516
|
-
let invite
|
|
517
522
|
try {
|
|
518
|
-
invite = await cache.invite.getCode(code)
|
|
523
|
+
const invite = await cache.invite.getCode(code)
|
|
524
|
+
invite.info.apps ??= {}
|
|
525
|
+
invite.info.apps[prodWorkspaceId] = role
|
|
526
|
+
|
|
527
|
+
await cache.invite.updateCode(code, invite)
|
|
528
|
+
ctx.body = { ...invite }
|
|
519
529
|
} catch (e) {
|
|
520
|
-
ctx.throw(400, "
|
|
530
|
+
ctx.throw(400, "Invitation is not valid or has expired.")
|
|
521
531
|
}
|
|
532
|
+
}
|
|
522
533
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
534
|
+
export const removeWorkspaceIdFromInvite = async (
|
|
535
|
+
ctx: UserCtx<void, UpdateInviteResponse, { code: string }>
|
|
536
|
+
) => {
|
|
537
|
+
const { code } = ctx.params
|
|
526
538
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
updated.info.builder = updateBody.builder
|
|
539
|
+
const workspaceId = await backendCoreUtils.getAppIdFromCtx(ctx)
|
|
540
|
+
if (!workspaceId) {
|
|
541
|
+
ctx.throw(400, "Workspace id not set")
|
|
531
542
|
}
|
|
543
|
+
const prodWorkspaceId = db.getProdWorkspaceID(workspaceId)
|
|
532
544
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
...invite.info,
|
|
538
|
-
apps: {
|
|
539
|
-
...invite.info.apps,
|
|
540
|
-
...updateBody.apps,
|
|
541
|
-
},
|
|
542
|
-
}
|
|
543
|
-
}
|
|
545
|
+
try {
|
|
546
|
+
const invite = await cache.invite.getCode(code)
|
|
547
|
+
invite.info.apps ??= {}
|
|
548
|
+
delete invite.info.apps[prodWorkspaceId]
|
|
544
549
|
|
|
545
|
-
|
|
546
|
-
|
|
550
|
+
await cache.invite.updateCode(code, invite)
|
|
551
|
+
ctx.body = { ...invite }
|
|
552
|
+
} catch (e) {
|
|
553
|
+
ctx.throw(400, "Invitation is not valid or has expired.")
|
|
554
|
+
}
|
|
547
555
|
}
|
|
548
556
|
|
|
549
557
|
export const inviteAccept = async (
|
|
@@ -614,3 +622,73 @@ export const inviteAccept = async (
|
|
|
614
622
|
ctx.throw(400, err || "Unable to create new user, invitation invalid.")
|
|
615
623
|
}
|
|
616
624
|
}
|
|
625
|
+
|
|
626
|
+
export const addUserToWorkspace = async (
|
|
627
|
+
ctx: UserCtx<
|
|
628
|
+
EditUserPermissionsResponse,
|
|
629
|
+
SaveUserResponse,
|
|
630
|
+
{ userId: string; role: string }
|
|
631
|
+
>
|
|
632
|
+
) => handleUserWorkspacePermission(ctx, ctx.params.userId, ctx.params.role)
|
|
633
|
+
|
|
634
|
+
export const removeUserFromWorkspace = async (
|
|
635
|
+
ctx: UserCtx<
|
|
636
|
+
EditUserPermissionsResponse,
|
|
637
|
+
SaveUserResponse,
|
|
638
|
+
{ userId: string }
|
|
639
|
+
>
|
|
640
|
+
) => handleUserWorkspacePermission(ctx, ctx.params.userId, undefined)
|
|
641
|
+
|
|
642
|
+
async function handleUserWorkspacePermission(
|
|
643
|
+
ctx: UserCtx<EditUserPermissionsResponse, SaveUserResponse>,
|
|
644
|
+
userId: string,
|
|
645
|
+
role: string | undefined
|
|
646
|
+
) {
|
|
647
|
+
const { _rev } = ctx.request.body
|
|
648
|
+
if (!_rev) {
|
|
649
|
+
ctx.throw(400, "rev is required")
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const currentUserId = ctx.user?._id
|
|
653
|
+
|
|
654
|
+
const workspaceId = await backendCoreUtils.getAppIdFromCtx(ctx)
|
|
655
|
+
if (!workspaceId) {
|
|
656
|
+
ctx.throw(400, "Workspace id not set")
|
|
657
|
+
}
|
|
658
|
+
const prodWorkspaceId = db.getProdWorkspaceID(workspaceId)
|
|
659
|
+
|
|
660
|
+
const existingUser = await users.getById(userId)
|
|
661
|
+
existingUser._rev = ctx.request.body._rev
|
|
662
|
+
if (role) {
|
|
663
|
+
existingUser.roles[prodWorkspaceId] = role
|
|
664
|
+
} else {
|
|
665
|
+
delete existingUser.roles[prodWorkspaceId]
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (role === "CREATOR" && !(await features.isAppBuildersEnabled())) {
|
|
669
|
+
throw new HTTPError("Feature not enabled, please check license", 400)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const appCreator = Object.entries(existingUser.roles)
|
|
673
|
+
.filter(([_appId, role]) => role === "CREATOR")
|
|
674
|
+
.map(([appId]) => appId)
|
|
675
|
+
if (!appCreator.length && existingUser.builder) {
|
|
676
|
+
delete existingUser.builder.creator
|
|
677
|
+
delete existingUser.builder.apps
|
|
678
|
+
} else if (appCreator.length) {
|
|
679
|
+
existingUser.builder ??= {}
|
|
680
|
+
existingUser.builder.creator = true
|
|
681
|
+
existingUser.builder.apps = appCreator
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const user = await userSdk.db.save(existingUser, {
|
|
685
|
+
currentUserId,
|
|
686
|
+
hashPassword: false,
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
ctx.body = {
|
|
690
|
+
_id: user._id!,
|
|
691
|
+
_rev: user._rev!,
|
|
692
|
+
email: user.email,
|
|
693
|
+
}
|
|
694
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { events } from "@budibase/backend-core"
|
|
2
2
|
import { generator } from "@budibase/backend-core/tests"
|
|
3
|
-
import { structures, TestConfiguration, mocks } from "../../../../tests"
|
|
4
3
|
import { User, UserGroup } from "@budibase/types"
|
|
4
|
+
import { mocks, structures, TestConfiguration } from "../../../../tests"
|
|
5
5
|
|
|
6
6
|
mocks.licenses.useGroups()
|
|
7
7
|
|
|
@@ -446,4 +446,92 @@ describe("/api/global/groups", () => {
|
|
|
446
446
|
})
|
|
447
447
|
})
|
|
448
448
|
})
|
|
449
|
+
|
|
450
|
+
describe("creator role functionality", () => {
|
|
451
|
+
let group: UserGroup
|
|
452
|
+
|
|
453
|
+
beforeEach(async () => {
|
|
454
|
+
mocks.licenses.useAppBuilders()
|
|
455
|
+
const groupResponse = await config.api.groups.saveGroup(
|
|
456
|
+
structures.groups.UserGroup()
|
|
457
|
+
)
|
|
458
|
+
group = groupResponse.body
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
describe("updateGroupApps with CREATOR role", () => {
|
|
462
|
+
it("should successfully update group with CREATOR role", async () => {
|
|
463
|
+
const appId = "app_test123"
|
|
464
|
+
|
|
465
|
+
// This should succeed without throwing an error
|
|
466
|
+
await config.api.groups.updateGroupApps(group._id!, {
|
|
467
|
+
add: [{ appId, roleId: "CREATOR" }],
|
|
468
|
+
remove: [],
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
const updatedGroup = await config.api.groups.find(group._id!)
|
|
472
|
+
expect(updatedGroup.body._id).toBe(group._id)
|
|
473
|
+
expect(updatedGroup.body.builder.apps).toEqual([appId])
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it("should fail to assign CREATOR role when feature is not enabled", async () => {
|
|
477
|
+
mocks.licenses.useCloudFree() // Disable app builders feature
|
|
478
|
+
const appId = "app_test123"
|
|
479
|
+
|
|
480
|
+
await config.api.groups.updateGroupApps(
|
|
481
|
+
group._id!,
|
|
482
|
+
{
|
|
483
|
+
add: [{ appId, roleId: "CREATOR" }],
|
|
484
|
+
remove: [],
|
|
485
|
+
},
|
|
486
|
+
{ expect: 400 }
|
|
487
|
+
)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it("should handle multiple CREATOR apps update operation", async () => {
|
|
491
|
+
const appId1 = "app_test111"
|
|
492
|
+
const appId2 = "app_test222"
|
|
493
|
+
const appId3 = "app_test333"
|
|
494
|
+
|
|
495
|
+
// This should succeed without throwing an error
|
|
496
|
+
await config.api.groups.updateGroupApps(group._id!, {
|
|
497
|
+
add: [
|
|
498
|
+
{ appId: appId1, roleId: "CREATOR" },
|
|
499
|
+
{ appId: appId2, roleId: "CREATOR" },
|
|
500
|
+
{ appId: appId3, roleId: "BASIC" }, // Not a creator role
|
|
501
|
+
],
|
|
502
|
+
remove: [],
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
// Verify the operation completed successfully
|
|
506
|
+
const updatedGroup = await config.api.groups.find(group._id!)
|
|
507
|
+
expect(updatedGroup.body._id).toBe(group._id)
|
|
508
|
+
expect(updatedGroup.body.builder.apps).toEqual([appId1, appId2])
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it("should handle removing apps from CREATOR group", async () => {
|
|
512
|
+
const appId1 = "app_test111"
|
|
513
|
+
const appId2 = "app_test222"
|
|
514
|
+
|
|
515
|
+
// First add CREATOR roles
|
|
516
|
+
await config.api.groups.updateGroupApps(group._id!, {
|
|
517
|
+
add: [
|
|
518
|
+
{ appId: appId1, roleId: "CREATOR" },
|
|
519
|
+
{ appId: appId2, roleId: "CREATOR" },
|
|
520
|
+
],
|
|
521
|
+
remove: [],
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// Then remove one app - this should succeed
|
|
525
|
+
await config.api.groups.updateGroupApps(group._id!, {
|
|
526
|
+
add: [],
|
|
527
|
+
remove: [{ appId: appId1 }],
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
// Verify the operation completed successfully
|
|
531
|
+
const updatedGroup = await config.api.groups.find(group._id!)
|
|
532
|
+
expect(updatedGroup.body._id).toBe(group._id)
|
|
533
|
+
expect(updatedGroup.body.builder.apps).toEqual([])
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
})
|
|
449
537
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InviteUsersResponse, OIDCUser, User } from "@budibase/types"
|
|
2
2
|
|
|
3
3
|
import { accounts as _accounts, events, tenancy } from "@budibase/backend-core"
|
|
4
|
+
import { mocks as featureMocks } from "@budibase/backend-core/tests"
|
|
4
5
|
import * as userSdk from "../../../../sdk/users"
|
|
5
6
|
import { TestConfiguration, mocks, structures } from "../../../../tests"
|
|
6
7
|
|
|
@@ -20,8 +21,11 @@ describe("/api/global/users", () => {
|
|
|
20
21
|
await config.afterAll()
|
|
21
22
|
})
|
|
22
23
|
|
|
23
|
-
beforeEach(() => {
|
|
24
|
+
beforeEach(async () => {
|
|
24
25
|
jest.clearAllMocks()
|
|
26
|
+
featureMocks.licenses.useCloudFree()
|
|
27
|
+
await mocks.licenses.setUsersQuota(1000)
|
|
28
|
+
await mocks.licenses.setCreatorsQuota(1000)
|
|
25
29
|
})
|
|
26
30
|
|
|
27
31
|
async function createBuilderUser() {
|
|
@@ -114,6 +118,222 @@ describe("/api/global/users", () => {
|
|
|
114
118
|
})
|
|
115
119
|
})
|
|
116
120
|
|
|
121
|
+
describe("POST /api/global/users/invite/:code/:role", () => {
|
|
122
|
+
it("should be able to add workspace id to invite", async () => {
|
|
123
|
+
const email = structures.users.newEmail()
|
|
124
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
125
|
+
sendMailMock,
|
|
126
|
+
email
|
|
127
|
+
)
|
|
128
|
+
const appId = "app_123456789"
|
|
129
|
+
const role = "BASIC"
|
|
130
|
+
|
|
131
|
+
const res = await config.withApp(appId, () =>
|
|
132
|
+
config.api.users.addWorkspaceIdToInvite(code, role)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
expect(res.body.info.apps).toBeDefined()
|
|
136
|
+
expect(res.body.info.apps[appId]).toBe(role)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("should handle invalid invite code", async () => {
|
|
140
|
+
const appId = "app_123456789"
|
|
141
|
+
const role = "BASIC"
|
|
142
|
+
|
|
143
|
+
await config.withApp(appId, () =>
|
|
144
|
+
config.api.users.addWorkspaceIdToInvite("invalid_code", role, 400)
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("should not allow builders to edit invites for apps they don't have access to", async () => {
|
|
149
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
150
|
+
sendMailMock,
|
|
151
|
+
structures.users.newEmail()
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Create a builder user with access to another app
|
|
155
|
+
const builderUser = await config.createUser({
|
|
156
|
+
...structures.users.user(),
|
|
157
|
+
builder: {
|
|
158
|
+
global: false,
|
|
159
|
+
apps: ["app_allowed_123"],
|
|
160
|
+
},
|
|
161
|
+
admin: { global: false },
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
await config.withUser(builderUser, () =>
|
|
165
|
+
config.withApp("app_no_access", () =>
|
|
166
|
+
config.api.users.addWorkspaceIdToInvite(code, "BASIC", 403)
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("should allow builders to edit invites for apps they have access to", async () => {
|
|
172
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
173
|
+
sendMailMock,
|
|
174
|
+
structures.users.newEmail()
|
|
175
|
+
)
|
|
176
|
+
const appId = "app_allowed_123"
|
|
177
|
+
const role = "BASIC"
|
|
178
|
+
|
|
179
|
+
// Create a builder user with access to the specific app
|
|
180
|
+
const builderUser = await config.createUser({
|
|
181
|
+
...structures.users.user(),
|
|
182
|
+
builder: {
|
|
183
|
+
global: false,
|
|
184
|
+
apps: [appId], // Has access to this specific app
|
|
185
|
+
},
|
|
186
|
+
admin: { global: false },
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
await config.login(builderUser)
|
|
190
|
+
const res = await config.withUser(builderUser, () =>
|
|
191
|
+
config.withApp(appId, () =>
|
|
192
|
+
config.api.users.addWorkspaceIdToInvite(code, role, 200)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
expect(res.body.info.apps[appId]).toBe(role)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it("should allow global builders to edit invites for any app", async () => {
|
|
199
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
200
|
+
sendMailMock,
|
|
201
|
+
structures.users.newEmail()
|
|
202
|
+
)
|
|
203
|
+
const appId = "app_any_123"
|
|
204
|
+
const role = "BASIC"
|
|
205
|
+
|
|
206
|
+
// Create a global builder user
|
|
207
|
+
const builderUser = await config.createUser({
|
|
208
|
+
...structures.users.user(),
|
|
209
|
+
builder: {
|
|
210
|
+
global: true,
|
|
211
|
+
},
|
|
212
|
+
admin: { global: false },
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
await config.login(builderUser)
|
|
216
|
+
const res = await config.withUser(builderUser, async () =>
|
|
217
|
+
config.withApp(appId, () =>
|
|
218
|
+
config.api.users.addWorkspaceIdToInvite(code, role, 200)
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
expect(res.body.info.apps[appId]).toBe(role)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe("DELETE /api/global/users/invite/:code", () => {
|
|
226
|
+
it("should be able to remove workspace id from invite", async () => {
|
|
227
|
+
const email = structures.users.newEmail()
|
|
228
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
229
|
+
sendMailMock,
|
|
230
|
+
email
|
|
231
|
+
)
|
|
232
|
+
const appId = "app_123456789"
|
|
233
|
+
const role = "BASIC"
|
|
234
|
+
|
|
235
|
+
// First add the workspace
|
|
236
|
+
await config.withApp(appId, () =>
|
|
237
|
+
config.api.users.addWorkspaceIdToInvite(code, role)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Then remove it
|
|
241
|
+
const res = await config.withApp(appId, () =>
|
|
242
|
+
config.api.users.removeWorkspaceIdFromInvite(code)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
expect(res.body.info.apps).toBeDefined()
|
|
246
|
+
expect(res.body.info.apps[appId]).toBeUndefined()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it("should handle removing non-existent workspace id", async () => {
|
|
250
|
+
const email = structures.users.newEmail()
|
|
251
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
252
|
+
sendMailMock,
|
|
253
|
+
email
|
|
254
|
+
)
|
|
255
|
+
const appId = "app_nonexistent"
|
|
256
|
+
|
|
257
|
+
const res = await config.withApp(appId, () =>
|
|
258
|
+
config.api.users.removeWorkspaceIdFromInvite(code)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
expect(res.body.info.apps).toBeDefined()
|
|
262
|
+
expect(res.body.info.apps[appId]).toBeUndefined()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("should handle invalid invite code", async () => {
|
|
266
|
+
const appId = "app_123456789"
|
|
267
|
+
|
|
268
|
+
await config.withApp(appId, () =>
|
|
269
|
+
config.api.users.removeWorkspaceIdFromInvite("invalid_code", 400)
|
|
270
|
+
)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it("should not allow builders to delete invites for apps they don't have access to", async () => {
|
|
274
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
275
|
+
sendMailMock,
|
|
276
|
+
structures.users.newEmail()
|
|
277
|
+
)
|
|
278
|
+
const appId = "app_no_access"
|
|
279
|
+
const role = "BASIC"
|
|
280
|
+
|
|
281
|
+
// First add the workspace as admin
|
|
282
|
+
await config.withApp(appId, () =>
|
|
283
|
+
config.api.users.addWorkspaceIdToInvite(code, role)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
// Create a builder user with specific app access
|
|
287
|
+
const builderUser = await config.createUser({
|
|
288
|
+
...structures.users.user(),
|
|
289
|
+
builder: {
|
|
290
|
+
global: false,
|
|
291
|
+
apps: ["app_allowed_123"], // Different app than the one being tested
|
|
292
|
+
},
|
|
293
|
+
admin: { global: false },
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
await config.login(builderUser)
|
|
297
|
+
await config.withUser(builderUser, () =>
|
|
298
|
+
config.withApp(appId, () =>
|
|
299
|
+
config.api.users.removeWorkspaceIdFromInvite(code, 403)
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it("should allow builders to delete invites for apps they have access to", async () => {
|
|
305
|
+
const { code } = await config.api.users.sendUserInvite(
|
|
306
|
+
sendMailMock,
|
|
307
|
+
structures.users.newEmail()
|
|
308
|
+
)
|
|
309
|
+
const appId = "app_allowed_456"
|
|
310
|
+
const role = "BASIC"
|
|
311
|
+
|
|
312
|
+
// First add the workspace as admin
|
|
313
|
+
await config.withApp(appId, () =>
|
|
314
|
+
config.api.users.addWorkspaceIdToInvite(code, role)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// Create a builder user with access to the specific app
|
|
318
|
+
const builderUser = await config.createUser({
|
|
319
|
+
...structures.users.user(),
|
|
320
|
+
builder: {
|
|
321
|
+
global: false,
|
|
322
|
+
apps: [appId], // Has access to this specific app
|
|
323
|
+
},
|
|
324
|
+
admin: { global: false },
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
await config.login(builderUser)
|
|
328
|
+
const res = await config.withUser(builderUser, () =>
|
|
329
|
+
config.withApp(appId, () =>
|
|
330
|
+
config.api.users.removeWorkspaceIdFromInvite(code, 200)
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
expect(res.body.info.apps[appId]).toBeUndefined()
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
117
337
|
describe("POST /api/global/users/multi/invite", () => {
|
|
118
338
|
it("should be able to generate an invitation", async () => {
|
|
119
339
|
const newUserInvite = () => ({
|
|
@@ -539,15 +759,17 @@ describe("/api/global/users", () => {
|
|
|
539
759
|
expect(response.body.message).toBe("Email address cannot be changed")
|
|
540
760
|
})
|
|
541
761
|
|
|
542
|
-
it("should allow a
|
|
762
|
+
it("should not allow a builder users to update an existing user", async () => {
|
|
543
763
|
const existingUser = await config.createUser(structures.users.user())
|
|
544
|
-
const
|
|
545
|
-
|
|
764
|
+
const builderUser = await config.createUser(
|
|
765
|
+
structures.users.builderUser()
|
|
766
|
+
)
|
|
767
|
+
await config.createSession(builderUser)
|
|
546
768
|
|
|
547
769
|
await config.api.users.saveUser(
|
|
548
770
|
existingUser,
|
|
549
|
-
|
|
550
|
-
config.authHeaders(
|
|
771
|
+
403,
|
|
772
|
+
config.authHeaders(builderUser)
|
|
551
773
|
)
|
|
552
774
|
})
|
|
553
775
|
|
|
@@ -763,6 +985,135 @@ describe("/api/global/users", () => {
|
|
|
763
985
|
})
|
|
764
986
|
})
|
|
765
987
|
|
|
988
|
+
describe("POST /api/global/users/:userId/permission/:role", () => {
|
|
989
|
+
it("should fail to assign CREATOR role when feature is not enabled", async () => {
|
|
990
|
+
const user = await config.createUser()
|
|
991
|
+
const workspaceId = "app_123456789"
|
|
992
|
+
|
|
993
|
+
const res = await config.withApp(workspaceId, () =>
|
|
994
|
+
config.api.users.addUserToWorkspace(
|
|
995
|
+
user._id!,
|
|
996
|
+
user._rev!,
|
|
997
|
+
"CREATOR",
|
|
998
|
+
400
|
|
999
|
+
)
|
|
1000
|
+
)
|
|
1001
|
+
expect(res.body.message).toBe("Feature not enabled, please check license")
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
it("should assign CREATOR role and set builder properties", async () => {
|
|
1005
|
+
featureMocks.licenses.useAppBuilders()
|
|
1006
|
+
|
|
1007
|
+
const user = await config.createUser()
|
|
1008
|
+
const workspaceId = "app_123456789"
|
|
1009
|
+
|
|
1010
|
+
await config.withApp(workspaceId, () =>
|
|
1011
|
+
config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR")
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
const updatedUser = await config.getUser(user.email)
|
|
1015
|
+
expect(updatedUser.roles[workspaceId]).toBe("CREATOR")
|
|
1016
|
+
expect(updatedUser.builder?.creator).toBe(true)
|
|
1017
|
+
expect(updatedUser.builder?.apps).toEqual([workspaceId])
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
it("should remove CREATOR role and clean up builder properties when no creator apps remain", async () => {
|
|
1021
|
+
mocks.licenses.useAppBuilders()
|
|
1022
|
+
const user = await config.createUser()
|
|
1023
|
+
const workspaceId = "app_123456789"
|
|
1024
|
+
|
|
1025
|
+
// First assign CREATOR role
|
|
1026
|
+
const res = await config.withApp(workspaceId, () =>
|
|
1027
|
+
config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR")
|
|
1028
|
+
)
|
|
1029
|
+
user._rev = res.body._rev
|
|
1030
|
+
|
|
1031
|
+
// Then remove the user from workspace
|
|
1032
|
+
await config.withApp(workspaceId, () =>
|
|
1033
|
+
config.api.users.removeUserFromWorkspace(user._id!, user._rev!)
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
const updatedUser = await config.getUser(user.email)
|
|
1037
|
+
expect(updatedUser.roles[workspaceId]).toBeUndefined()
|
|
1038
|
+
expect(updatedUser.builder?.creator).toBeUndefined()
|
|
1039
|
+
expect(updatedUser.builder?.apps).toBeUndefined()
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
it("should maintain builder properties when user has multiple CREATOR roles", async () => {
|
|
1043
|
+
mocks.licenses.useAppBuilders()
|
|
1044
|
+
const user = await config.createUser()
|
|
1045
|
+
const workspaceId1 = "app_111111111"
|
|
1046
|
+
const workspaceId2 = "app_222222222"
|
|
1047
|
+
|
|
1048
|
+
// Assign CREATOR role to two workspaces
|
|
1049
|
+
let res = await config.withApp(workspaceId1, () =>
|
|
1050
|
+
config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR")
|
|
1051
|
+
)
|
|
1052
|
+
user._rev = res.body._rev
|
|
1053
|
+
res = await config.withApp(workspaceId2, () =>
|
|
1054
|
+
config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR")
|
|
1055
|
+
)
|
|
1056
|
+
user._rev = res.body._rev
|
|
1057
|
+
|
|
1058
|
+
let updatedUser = await config.getUser(user.email)
|
|
1059
|
+
expect(updatedUser.roles[workspaceId1]).toBe("CREATOR")
|
|
1060
|
+
expect(updatedUser.roles[workspaceId2]).toBe("CREATOR")
|
|
1061
|
+
expect(updatedUser.builder?.creator).toBe(true)
|
|
1062
|
+
expect(updatedUser.builder?.apps).toEqual(
|
|
1063
|
+
expect.arrayContaining([workspaceId1, workspaceId2])
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
// Remove from one workspace
|
|
1067
|
+
await config.withApp(workspaceId1, () =>
|
|
1068
|
+
config.api.users.removeUserFromWorkspace(user._id!, user._rev!)
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
updatedUser = await config.getUser(user.email)
|
|
1072
|
+
expect(updatedUser.roles[workspaceId1]).toBeUndefined()
|
|
1073
|
+
expect(updatedUser.roles[workspaceId2]).toBe("CREATOR")
|
|
1074
|
+
expect(updatedUser.builder?.creator).toBe(true)
|
|
1075
|
+
expect(updatedUser.builder?.apps).toEqual([workspaceId2])
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
it("should handle non-CREATOR role assignments without affecting builder properties", async () => {
|
|
1079
|
+
const user = await config.createUser()
|
|
1080
|
+
const workspaceId = "app_123456789"
|
|
1081
|
+
|
|
1082
|
+
const res = await config.withApp(workspaceId, () =>
|
|
1083
|
+
config.api.users.addUserToWorkspace(user._id!, user._rev!, "BASIC")
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
expect(res.body._id).toBe(user._id)
|
|
1087
|
+
|
|
1088
|
+
const updatedUser = await config.getUser(user.email)
|
|
1089
|
+
expect(updatedUser.roles[workspaceId]).toBe("BASIC")
|
|
1090
|
+
expect(updatedUser.builder?.creator).toBeUndefined()
|
|
1091
|
+
expect(updatedUser.builder?.apps).toBeUndefined()
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
it("should not allow non-admin users to modify workspace permissions", async () => {
|
|
1095
|
+
const regularUser = await config.createUser()
|
|
1096
|
+
const targetUser = await config.createUser()
|
|
1097
|
+
const workspaceId = "app_123456789"
|
|
1098
|
+
|
|
1099
|
+
await config.login(regularUser)
|
|
1100
|
+
|
|
1101
|
+
const res = await config.withUser(regularUser, () =>
|
|
1102
|
+
config.withApp(workspaceId, () =>
|
|
1103
|
+
config.api.users.addUserToWorkspace(
|
|
1104
|
+
targetUser._id!,
|
|
1105
|
+
targetUser._rev!,
|
|
1106
|
+
"CREATOR",
|
|
1107
|
+
403
|
|
1108
|
+
)
|
|
1109
|
+
)
|
|
1110
|
+
)
|
|
1111
|
+
expect(res.body.message).toBe(
|
|
1112
|
+
"Workspace Admin/Builder user only endpoint."
|
|
1113
|
+
)
|
|
1114
|
+
})
|
|
1115
|
+
})
|
|
1116
|
+
|
|
766
1117
|
describe("DELETE /api/global/users/:userId", () => {
|
|
767
1118
|
it("should be able to destroy a basic user", async () => {
|
|
768
1119
|
const user = await config.createUser()
|
|
@@ -44,14 +44,6 @@ function buildInviteMultipleValidation() {
|
|
|
44
44
|
))
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const createUserAdminOnly = (ctx: any, next: any) => {
|
|
48
|
-
if (!ctx.request.body._id) {
|
|
49
|
-
return auth.adminOnly(ctx, next)
|
|
50
|
-
} else {
|
|
51
|
-
return auth.builderOrAdmin(ctx, next)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
47
|
function buildInviteAcceptValidation() {
|
|
56
48
|
// prettier-ignore
|
|
57
49
|
return auth.joiValidator.body(Joi.object({
|
|
@@ -102,14 +94,30 @@ builderOrAdminRoutes
|
|
|
102
94
|
.get("/api/global/users/count/:appId", controller.countByApp)
|
|
103
95
|
.get("/api/global/users/invites", controller.getUserInvites)
|
|
104
96
|
.get("/api/global/users/:id", controller.find)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
.post(
|
|
98
|
+
"/api/global/users/invite/:code/:role",
|
|
99
|
+
controller.addWorkspaceIdToInvite
|
|
100
|
+
)
|
|
101
|
+
.delete(
|
|
102
|
+
"/api/global/users/invite/:code",
|
|
103
|
+
controller.removeWorkspaceIdFromInvite
|
|
104
|
+
)
|
|
108
105
|
.post(
|
|
109
106
|
"/api/global/users/onboard",
|
|
110
107
|
buildInviteMultipleValidation(),
|
|
111
108
|
controller.onboardUsers
|
|
112
109
|
)
|
|
110
|
+
.post(
|
|
111
|
+
"/api/global/users/:userId/permission/:role",
|
|
112
|
+
controller.addUserToWorkspace
|
|
113
|
+
)
|
|
114
|
+
.delete(
|
|
115
|
+
"/api/global/users/:userId/permission",
|
|
116
|
+
controller.removeUserFromWorkspace
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
adminRoutes
|
|
120
|
+
.post("/api/global/users/invite", buildInviteValidation(), controller.invite)
|
|
113
121
|
.post(
|
|
114
122
|
"/api/global/users/multi/invite",
|
|
115
123
|
buildInviteMultipleValidation(),
|
|
@@ -119,17 +127,11 @@ adminRoutes
|
|
|
119
127
|
"/api/global/users/multi/invite/delete",
|
|
120
128
|
controller.removeMultipleInvites
|
|
121
129
|
)
|
|
122
|
-
.post("/api/global/users
|
|
130
|
+
.post("/api/global/users", users.buildUserSaveValidation(), controller.save)
|
|
123
131
|
|
|
124
132
|
loggedInRoutes
|
|
125
133
|
// search can be used by any user now, to retrieve users for user column
|
|
126
134
|
.post("/api/global/users/search", controller.search)
|
|
127
|
-
.post(
|
|
128
|
-
"/api/global/users",
|
|
129
|
-
createUserAdminOnly,
|
|
130
|
-
users.buildUserSaveValidation(),
|
|
131
|
-
controller.save
|
|
132
|
-
)
|
|
133
135
|
// non-global endpoints
|
|
134
136
|
.get("/api/global/users/invite/:code", controller.checkInvite)
|
|
135
137
|
.post(
|
package/src/api/routes/index.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import Router from "@koa/router"
|
|
2
1
|
import { api as pro } from "@budibase/pro"
|
|
2
|
+
import Router from "@koa/router"
|
|
3
3
|
import { endpointGroupList } from "./endpointGroups"
|
|
4
4
|
|
|
5
5
|
// import to register the routes with the endpoint groups
|
|
6
6
|
import "./global/auth"
|
|
7
|
-
import "./global/users"
|
|
8
7
|
import "./global/configs"
|
|
9
|
-
import "./global/templates"
|
|
10
8
|
import "./global/email"
|
|
11
|
-
import "./global/roles"
|
|
12
9
|
import "./global/events"
|
|
13
|
-
import "./system/environment"
|
|
14
|
-
import "./system/tenants"
|
|
15
|
-
import "./system/status"
|
|
16
|
-
import "./global/self"
|
|
17
10
|
import "./global/license"
|
|
11
|
+
import "./global/roles"
|
|
12
|
+
import "./global/self"
|
|
13
|
+
import "./global/templates"
|
|
14
|
+
import "./global/users"
|
|
18
15
|
import "./system/accounts"
|
|
19
|
-
import "./system/
|
|
16
|
+
import "./system/environment"
|
|
20
17
|
import "./system/logs"
|
|
18
|
+
import "./system/restore"
|
|
19
|
+
import "./system/status"
|
|
20
|
+
import "./system/tenants"
|
|
21
21
|
|
|
22
22
|
const endpointGroupsRouter = new Router()
|
|
23
23
|
for (let endpoint of endpointGroupList.listAllEndpoints()) {
|
|
@@ -26,7 +26,6 @@ for (let endpoint of endpointGroupList.listAllEndpoints()) {
|
|
|
26
26
|
|
|
27
27
|
export const routes: Router[] = [
|
|
28
28
|
endpointGroupsRouter,
|
|
29
|
-
pro.users,
|
|
30
29
|
pro.groups,
|
|
31
30
|
pro.auditLogs,
|
|
32
31
|
pro.scim,
|
package/src/index.ts
CHANGED
|
@@ -3,31 +3,31 @@ if (process.env.DD_APM_ENABLED) {
|
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
// need to load environment first
|
|
6
|
-
import env from "./environment"
|
|
7
|
-
import Application, { Middleware } from "koa"
|
|
8
|
-
import { bootstrap } from "global-agent"
|
|
9
|
-
import * as db from "./db"
|
|
10
|
-
import { sdk as proSdk } from "@budibase/pro"
|
|
11
6
|
import {
|
|
12
7
|
auth,
|
|
13
|
-
|
|
8
|
+
cache,
|
|
9
|
+
env as coreEnv,
|
|
14
10
|
events,
|
|
11
|
+
features,
|
|
12
|
+
logging,
|
|
15
13
|
middleware,
|
|
16
14
|
queue,
|
|
17
|
-
env as coreEnv,
|
|
18
|
-
timers,
|
|
19
15
|
redis,
|
|
20
|
-
|
|
21
|
-
features,
|
|
16
|
+
timers,
|
|
22
17
|
} from "@budibase/backend-core"
|
|
18
|
+
import { sdk as proSdk } from "@budibase/pro"
|
|
19
|
+
import { bootstrap } from "global-agent"
|
|
20
|
+
import http from "http"
|
|
21
|
+
import gracefulShutdown from "http-graceful-shutdown"
|
|
22
|
+
import Application, { Middleware } from "koa"
|
|
23
|
+
import koaBody from "koa-body"
|
|
23
24
|
import RedisStore from "koa-redis"
|
|
25
|
+
import api from "./api"
|
|
24
26
|
import { loadTemplateConfig } from "./constants/templates"
|
|
27
|
+
import * as db from "./db"
|
|
28
|
+
import env from "./environment"
|
|
25
29
|
|
|
26
30
|
db.init()
|
|
27
|
-
import koaBody from "koa-body"
|
|
28
|
-
import http from "http"
|
|
29
|
-
import api from "./api"
|
|
30
|
-
import gracefulShutdown from "http-graceful-shutdown"
|
|
31
31
|
|
|
32
32
|
const koaSession = require("koa-session")
|
|
33
33
|
|
|
@@ -52,7 +52,12 @@ app.proxy = true
|
|
|
52
52
|
|
|
53
53
|
// set up top level koa middleware
|
|
54
54
|
app.use(handleScimBody)
|
|
55
|
-
app.use(
|
|
55
|
+
app.use(
|
|
56
|
+
koaBody({
|
|
57
|
+
multipart: true,
|
|
58
|
+
parsedMethods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
59
|
+
})
|
|
60
|
+
)
|
|
56
61
|
|
|
57
62
|
let store: any
|
|
58
63
|
|
|
@@ -51,6 +51,7 @@ class TestConfiguration {
|
|
|
51
51
|
apiKey?: string
|
|
52
52
|
userPassword = "password123!"
|
|
53
53
|
sessions: string[] = []
|
|
54
|
+
appId?: string
|
|
54
55
|
|
|
55
56
|
constructor(opts: { openServer: boolean } = { openServer: true }) {
|
|
56
57
|
// default to cloud hosting
|
|
@@ -233,6 +234,16 @@ class TestConfiguration {
|
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
async withApp<T>(appId: string, f: () => Promise<T>): Promise<T> {
|
|
238
|
+
const oldAppId = this.appId
|
|
239
|
+
this.appId = appId
|
|
240
|
+
try {
|
|
241
|
+
return await f()
|
|
242
|
+
} finally {
|
|
243
|
+
this.appId = oldAppId
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
236
247
|
async login(user: User) {
|
|
237
248
|
await this.createSession(user)
|
|
238
249
|
return this.authHeaders(user)
|
|
@@ -245,11 +256,15 @@ class TestConfiguration {
|
|
|
245
256
|
tenantId: user.tenantId,
|
|
246
257
|
}
|
|
247
258
|
const authCookie = jwt.sign(authToken, coreEnv.JWT_SECRET as Secret)
|
|
248
|
-
|
|
259
|
+
const headers: Record<string, string> = {
|
|
249
260
|
Accept: "application/json",
|
|
250
261
|
...this.cookieHeader([`${constants.Cookie.Auth}=${authCookie}`]),
|
|
251
262
|
[constants.Header.CSRF_TOKEN]: CSRF_TOKEN,
|
|
252
263
|
}
|
|
264
|
+
if (this.appId) {
|
|
265
|
+
headers[constants.Header.APP_ID] = this.appId
|
|
266
|
+
}
|
|
267
|
+
return headers
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
defaultHeaders() {
|
package/src/tests/api/users.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { generator } from "@budibase/backend-core/tests"
|
|
1
2
|
import {
|
|
2
|
-
BulkUserResponse,
|
|
3
3
|
BulkUserRequest,
|
|
4
|
-
|
|
5
|
-
User,
|
|
4
|
+
BulkUserResponse,
|
|
6
5
|
CreateAdminUserRequest,
|
|
7
|
-
|
|
6
|
+
InviteUsersRequest,
|
|
8
7
|
InviteUsersResponse,
|
|
8
|
+
SearchFilters,
|
|
9
|
+
User,
|
|
9
10
|
} from "@budibase/types"
|
|
10
11
|
import structures from "../structures"
|
|
11
|
-
import { generator } from "@budibase/backend-core/tests"
|
|
12
12
|
import { TestAPI, TestAPIOpts } from "./base"
|
|
13
13
|
|
|
14
14
|
export class UserAPI extends TestAPI {
|
|
@@ -179,22 +179,6 @@ export class UserAPI extends TestAPI {
|
|
|
179
179
|
.expect(opts?.status ? opts.status : 200)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
grantBuilderToApp = (userId: string, appId: string, statusCode = 200) => {
|
|
183
|
-
return this.request
|
|
184
|
-
.post(`/api/global/users/${userId}/app/${appId}/builder`)
|
|
185
|
-
.set(this.config.defaultHeaders())
|
|
186
|
-
.expect("Content-Type", /json/)
|
|
187
|
-
.expect(statusCode)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
revokeBuilderFromApp = (userId: string, appId: string) => {
|
|
191
|
-
return this.request
|
|
192
|
-
.delete(`/api/global/users/${userId}/app/${appId}/builder`)
|
|
193
|
-
.set(this.config.defaultHeaders())
|
|
194
|
-
.expect("Content-Type", /json/)
|
|
195
|
-
.expect(200)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
182
|
onboardUser = async (
|
|
199
183
|
req: InviteUsersRequest
|
|
200
184
|
): Promise<InviteUsersResponse> => {
|
|
@@ -227,4 +211,43 @@ export class UserAPI extends TestAPI {
|
|
|
227
211
|
.set(this.config.internalAPIHeaders())
|
|
228
212
|
.expect(status)
|
|
229
213
|
}
|
|
214
|
+
|
|
215
|
+
addWorkspaceIdToInvite = (code: string, role: string, status = 200) => {
|
|
216
|
+
return this.request
|
|
217
|
+
.post(`/api/global/users/invite/${code}/${role}`)
|
|
218
|
+
.set(this.config.defaultHeaders())
|
|
219
|
+
.expect("Content-Type", /json/)
|
|
220
|
+
.expect(status)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
removeWorkspaceIdFromInvite = (code: string, status = 200) => {
|
|
224
|
+
return this.request
|
|
225
|
+
.delete(`/api/global/users/invite/${code}`)
|
|
226
|
+
.set(this.config.defaultHeaders())
|
|
227
|
+
.expect("Content-Type", /json/)
|
|
228
|
+
.expect(status)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
addUserToWorkspace = (
|
|
232
|
+
userId: string,
|
|
233
|
+
_rev: string,
|
|
234
|
+
role: string,
|
|
235
|
+
status = 200
|
|
236
|
+
) => {
|
|
237
|
+
return this.request
|
|
238
|
+
.post(`/api/global/users/${userId}/permission/${role}`)
|
|
239
|
+
.send({ _rev })
|
|
240
|
+
.set(this.config.defaultHeaders())
|
|
241
|
+
.expect("Content-Type", /json/)
|
|
242
|
+
.expect(status)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
removeUserFromWorkspace = (userId: string, _rev: string, status = 200) => {
|
|
246
|
+
return this.request
|
|
247
|
+
.delete(`/api/global/users/${userId}/permission`)
|
|
248
|
+
.send({ _rev })
|
|
249
|
+
.set(this.config.defaultHeaders())
|
|
250
|
+
.expect("Content-Type", /json/)
|
|
251
|
+
.expect(status)
|
|
252
|
+
}
|
|
230
253
|
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { TestConfiguration, structures } from "../../../../tests"
|
|
2
|
-
import { mocks } from "@budibase/backend-core/tests"
|
|
3
|
-
import { User } from "@budibase/types"
|
|
4
|
-
|
|
5
|
-
const MOCK_APP_ID = "app_a"
|
|
6
|
-
|
|
7
|
-
describe("/api/global/users/:userId/app/builder", () => {
|
|
8
|
-
const config = new TestConfiguration()
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
await config.beforeAll()
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
afterAll(async () => {
|
|
15
|
-
await config.afterAll()
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
async function newUser() {
|
|
19
|
-
const base = structures.users.user()
|
|
20
|
-
return await config.createUser(base)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function getUser(userId: string) {
|
|
24
|
-
const response = await config.api.users.getUser(userId)
|
|
25
|
-
return response.body as User
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("Confirm pro license", () => {
|
|
29
|
-
it("should 400 with licensing", async () => {
|
|
30
|
-
const user = await newUser()
|
|
31
|
-
const resp = await config.api.users.grantBuilderToApp(
|
|
32
|
-
user._id!,
|
|
33
|
-
MOCK_APP_ID,
|
|
34
|
-
400
|
|
35
|
-
)
|
|
36
|
-
expect(resp.body.message).toContain(
|
|
37
|
-
"appBuilders are not currently enabled"
|
|
38
|
-
)
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe("PATCH /api/global/users/:userId/app/:appId/builder", () => {
|
|
43
|
-
it("should be able to grant a user access to a particular app", async () => {
|
|
44
|
-
mocks.licenses.useAppBuilders()
|
|
45
|
-
const user = await newUser()
|
|
46
|
-
await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID)
|
|
47
|
-
const updated = await getUser(user._id!)
|
|
48
|
-
expect(updated.builder?.apps![0]).toBe(MOCK_APP_ID)
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe("DELETE /api/global/users/:userId/app/:appId/builder", () => {
|
|
53
|
-
it("should allow revoking access", async () => {
|
|
54
|
-
mocks.licenses.useAppBuilders()
|
|
55
|
-
const user = await newUser()
|
|
56
|
-
await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID)
|
|
57
|
-
let updated = await getUser(user._id!)
|
|
58
|
-
expect(updated.builder?.apps![0]).toBe(MOCK_APP_ID)
|
|
59
|
-
await config.api.users.revokeBuilderFromApp(user._id!, MOCK_APP_ID)
|
|
60
|
-
updated = await getUser(user._id!)
|
|
61
|
-
expect(updated.builder?.apps!.length).toBe(0)
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
})
|