@budibase/worker 3.12.5 → 3.12.6
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/scripts/dev/manage.js +1 -0
- package/src/api/controllers/global/configs.ts +78 -41
- package/src/api/controllers/global/users.ts +28 -13
- package/src/api/routes/global/tests/configs.spec.ts +122 -84
- package/src/api/routes/global/tests/users.spec.ts +18 -0
- package/src/tests/TestConfiguration.ts +20 -7
- package/src/tests/api/configs.ts +8 -3
- package/src/tests/api/users.ts +8 -2
- package/src/tests/structures/configs.ts +9 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "3.12.
|
|
4
|
+
"version": "3.12.6",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -123,5 +123,5 @@
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
|
-
"gitHead": "
|
|
126
|
+
"gitHead": "a9dabfd71bc85ee7bff3ad4c1421e1478987d0a2"
|
|
127
127
|
}
|
package/scripts/dev/manage.js
CHANGED
|
@@ -21,6 +21,7 @@ async function init() {
|
|
|
21
21
|
MULTI_TENANCY: "",
|
|
22
22
|
DISABLE_ACCOUNT_PORTAL: "1",
|
|
23
23
|
ACCOUNT_PORTAL_URL: "http://localhost:10001",
|
|
24
|
+
ACCOUNT_PORTAL_API_KEY: "budibase",
|
|
24
25
|
PLATFORM_URL: "http://localhost:10000",
|
|
25
26
|
APPS_URL: "http://localhost:4001",
|
|
26
27
|
SERVICE: "worker-service",
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
} from "@budibase/backend-core"
|
|
14
14
|
import { checkAnyUserExists } from "../../../utilities/users"
|
|
15
15
|
import {
|
|
16
|
-
AIConfig,
|
|
17
16
|
AIInnerConfig,
|
|
18
17
|
Config,
|
|
19
18
|
ConfigChecklistResponse,
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
SaveConfigResponse,
|
|
37
36
|
SettingsBrandingConfig,
|
|
38
37
|
SettingsInnerConfig,
|
|
38
|
+
SMTPInnerConfig,
|
|
39
39
|
SSOConfig,
|
|
40
40
|
SSOConfigType,
|
|
41
41
|
UploadConfigFileResponse,
|
|
@@ -158,7 +158,23 @@ async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
|
|
|
158
158
|
return !!Object.values(ssoConfigs).find(c => c?.activated)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
async function
|
|
161
|
+
async function processSMTPConfig(
|
|
162
|
+
config: SMTPInnerConfig,
|
|
163
|
+
existingConfig?: SMTPInnerConfig
|
|
164
|
+
) {
|
|
165
|
+
await email.verifyConfig(config)
|
|
166
|
+
if (config.auth?.pass === PASSWORD_REPLACEMENT) {
|
|
167
|
+
// if the password is being replaced, use the existing password
|
|
168
|
+
if (existingConfig && existingConfig.auth?.pass) {
|
|
169
|
+
config.auth.pass = existingConfig.auth.pass
|
|
170
|
+
} else {
|
|
171
|
+
// otherwise, throw an error
|
|
172
|
+
throw new BadRequestError("SMTP password is required")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function processSettingsConfig(
|
|
162
178
|
config: SettingsInnerConfig & SettingsBrandingConfig,
|
|
163
179
|
existingConfig?: SettingsInnerConfig & SettingsBrandingConfig
|
|
164
180
|
) {
|
|
@@ -203,19 +219,37 @@ async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) {
|
|
|
203
219
|
}
|
|
204
220
|
}
|
|
205
221
|
|
|
206
|
-
async function
|
|
222
|
+
async function processGoogleConfig(
|
|
223
|
+
config: GoogleInnerConfig,
|
|
224
|
+
existing?: GoogleInnerConfig
|
|
225
|
+
) {
|
|
207
226
|
await verifySSOConfig(ConfigType.GOOGLE, config)
|
|
227
|
+
|
|
228
|
+
if (existing && config.clientSecret === PASSWORD_REPLACEMENT) {
|
|
229
|
+
config.clientSecret = existing.clientSecret
|
|
230
|
+
}
|
|
208
231
|
}
|
|
209
232
|
|
|
210
|
-
async function
|
|
233
|
+
async function processOIDCConfig(config: OIDCConfigs, existing?: OIDCConfigs) {
|
|
211
234
|
await verifySSOConfig(ConfigType.OIDC, config.configs[0])
|
|
235
|
+
|
|
236
|
+
if (existing) {
|
|
237
|
+
for (const c of config.configs) {
|
|
238
|
+
const existingConfig = existing.configs.find(e => e.uuid === c.uuid)
|
|
239
|
+
if (!existingConfig) {
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if (c.clientSecret === PASSWORD_REPLACEMENT) {
|
|
243
|
+
c.clientSecret = existingConfig.clientSecret
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
212
247
|
}
|
|
213
248
|
|
|
214
249
|
export async function processAIConfig(
|
|
215
250
|
newConfig: AIInnerConfig,
|
|
216
251
|
existingConfig: AIInnerConfig
|
|
217
252
|
) {
|
|
218
|
-
// ensure that the redacted API keys are not overwritten in the DB
|
|
219
253
|
for (const key in existingConfig) {
|
|
220
254
|
if (newConfig[key]?.apiKey === PASSWORD_REPLACEMENT) {
|
|
221
255
|
newConfig[key].apiKey = existingConfig[key].apiKey
|
|
@@ -256,16 +290,16 @@ export async function save(
|
|
|
256
290
|
try {
|
|
257
291
|
switch (type) {
|
|
258
292
|
case ConfigType.SMTP:
|
|
259
|
-
await
|
|
293
|
+
await processSMTPConfig(config, existingConfig?.config)
|
|
260
294
|
break
|
|
261
295
|
case ConfigType.SETTINGS:
|
|
262
|
-
await
|
|
296
|
+
await processSettingsConfig(config, existingConfig?.config)
|
|
263
297
|
break
|
|
264
298
|
case ConfigType.GOOGLE:
|
|
265
|
-
await
|
|
299
|
+
await processGoogleConfig(config, existingConfig?.config)
|
|
266
300
|
break
|
|
267
301
|
case ConfigType.OIDC:
|
|
268
|
-
await
|
|
302
|
+
await processOIDCConfig(config, existingConfig?.config)
|
|
269
303
|
break
|
|
270
304
|
case ConfigType.AI:
|
|
271
305
|
if (existingConfig) {
|
|
@@ -353,45 +387,48 @@ async function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
|
|
|
353
387
|
}
|
|
354
388
|
|
|
355
389
|
export async function find(ctx: UserCtx<void, FindConfigResponse>) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
let scopedConfig = await configs.getConfig(type)
|
|
360
|
-
|
|
361
|
-
if (scopedConfig) {
|
|
362
|
-
await handleConfigType(type, scopedConfig)
|
|
363
|
-
} else if (type === ConfigType.AI) {
|
|
364
|
-
scopedConfig = { type: ConfigType.AI, config: {} }
|
|
365
|
-
await handleAIConfig(scopedConfig)
|
|
366
|
-
} else {
|
|
367
|
-
// If no config found and not AI type, just return an empty body
|
|
368
|
-
ctx.body = {}
|
|
369
|
-
return
|
|
370
|
-
}
|
|
390
|
+
// Find the config with the most granular scope based on context
|
|
391
|
+
const type = ctx.params.type
|
|
392
|
+
let config = await configs.getConfig(type)
|
|
371
393
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
394
|
+
if (!config && type === ConfigType.AI) {
|
|
395
|
+
config = { type: ConfigType.AI, config: {} }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!config) {
|
|
399
|
+
ctx.body = {}
|
|
400
|
+
return
|
|
375
401
|
}
|
|
376
|
-
}
|
|
377
402
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
403
|
+
switch (type) {
|
|
404
|
+
case ConfigType.OIDC_LOGOS:
|
|
405
|
+
await enrichOIDCLogos(config)
|
|
406
|
+
break
|
|
407
|
+
case ConfigType.AI:
|
|
408
|
+
await pro.sdk.ai.enrichAIConfig(config)
|
|
409
|
+
break
|
|
383
410
|
}
|
|
384
|
-
}
|
|
385
411
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
stripApiKeys(config)
|
|
412
|
+
stripSecrets(config)
|
|
413
|
+
ctx.body = config
|
|
389
414
|
}
|
|
390
415
|
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
config.config[key].apiKey
|
|
416
|
+
function stripSecrets(config: Config) {
|
|
417
|
+
if (isAIConfig(config)) {
|
|
418
|
+
for (const key in config.config) {
|
|
419
|
+
if (config.config[key].apiKey) {
|
|
420
|
+
config.config[key].apiKey = PASSWORD_REPLACEMENT
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else if (isSMTPConfig(config)) {
|
|
424
|
+
if (config.config.auth?.pass) {
|
|
425
|
+
config.config.auth.pass = PASSWORD_REPLACEMENT
|
|
426
|
+
}
|
|
427
|
+
} else if (isGoogleConfig(config)) {
|
|
428
|
+
config.config.clientSecret = PASSWORD_REPLACEMENT
|
|
429
|
+
} else if (isOIDCConfig(config)) {
|
|
430
|
+
for (const c of config.config.configs) {
|
|
431
|
+
c.clientSecret = PASSWORD_REPLACEMENT
|
|
395
432
|
}
|
|
396
433
|
}
|
|
397
434
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
SaveUserResponse,
|
|
34
34
|
SearchUsersRequest,
|
|
35
35
|
SearchUsersResponse,
|
|
36
|
+
StrippedUser,
|
|
36
37
|
UnsavedUser,
|
|
37
38
|
UpdateInviteRequest,
|
|
38
39
|
UpdateInviteResponse,
|
|
@@ -66,6 +67,15 @@ const generatePassword = (length: number) => {
|
|
|
66
67
|
.slice(0, length)
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
const stripUsers = (users: (User | StrippedUser)[]): StrippedUser[] => {
|
|
71
|
+
return users.map(user => ({
|
|
72
|
+
_id: user._id,
|
|
73
|
+
email: user.email,
|
|
74
|
+
tenantId: user.tenantId,
|
|
75
|
+
userId: user.userId,
|
|
76
|
+
}))
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
export const save = async (ctx: UserCtx<UnsavedUser, SaveUserResponse>) => {
|
|
70
80
|
try {
|
|
71
81
|
const currentUserId = ctx.user?._id
|
|
@@ -249,18 +259,8 @@ export const destroy = async (ctx: UserCtx<void, DeleteUserResponse>) => {
|
|
|
249
259
|
}
|
|
250
260
|
}
|
|
251
261
|
|
|
252
|
-
export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
|
|
253
|
-
const body = ctx.request.body
|
|
254
|
-
const users = await userSdk.db.getUsersByAppAccess({
|
|
255
|
-
appId: body.appId,
|
|
256
|
-
limit: body.limit,
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
ctx.body = { data: users }
|
|
260
|
-
}
|
|
261
|
-
|
|
262
262
|
export const search = async (
|
|
263
|
-
ctx:
|
|
263
|
+
ctx: UserCtx<SearchUsersRequest, SearchUsersResponse>
|
|
264
264
|
) => {
|
|
265
265
|
const body = ctx.request.body
|
|
266
266
|
|
|
@@ -287,8 +287,13 @@ export const search = async (
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
let response: SearchUsersResponse = { data: [] }
|
|
291
|
+
|
|
290
292
|
if (body.paginate === false) {
|
|
291
|
-
await
|
|
293
|
+
response.data = await userSdk.db.getUsersByAppAccess({
|
|
294
|
+
appId: body.appId,
|
|
295
|
+
limit: body.limit,
|
|
296
|
+
})
|
|
292
297
|
} else {
|
|
293
298
|
const paginated = await userSdk.core.paginatedUsers(body)
|
|
294
299
|
// user hashed password shouldn't ever be returned
|
|
@@ -297,8 +302,18 @@ export const search = async (
|
|
|
297
302
|
delete user.password
|
|
298
303
|
}
|
|
299
304
|
}
|
|
300
|
-
|
|
305
|
+
response = {
|
|
306
|
+
data: paginated.data,
|
|
307
|
+
hasNextPage: paginated.hasNextPage,
|
|
308
|
+
nextPage: paginated.nextPage,
|
|
309
|
+
}
|
|
301
310
|
}
|
|
311
|
+
|
|
312
|
+
if (!users.hasBuilderPermissions(ctx.user)) {
|
|
313
|
+
response.data = stripUsers(response.data)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
ctx.body = response
|
|
302
317
|
}
|
|
303
318
|
|
|
304
319
|
// called internally by app server user fetch
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
// mock the email system
|
|
2
1
|
jest.mock("nodemailer")
|
|
3
2
|
import { TestConfiguration, structures, mocks } from "../../../../tests"
|
|
4
3
|
|
|
5
4
|
mocks.email.mock()
|
|
6
|
-
import { events } from "@budibase/backend-core"
|
|
5
|
+
import { configs, events } from "@budibase/backend-core"
|
|
7
6
|
import { GetPublicSettingsResponse, Config, ConfigType } from "@budibase/types"
|
|
8
7
|
|
|
8
|
+
const { google, smtp, settings, oidc } = structures.configs
|
|
9
|
+
|
|
9
10
|
describe("configs", () => {
|
|
10
11
|
const config = new TestConfiguration()
|
|
11
12
|
|
|
12
13
|
beforeEach(async () => {
|
|
13
14
|
await config.beforeAll()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
15
|
jest.clearAllMocks()
|
|
18
16
|
})
|
|
19
17
|
|
|
@@ -22,38 +20,20 @@ describe("configs", () => {
|
|
|
22
20
|
})
|
|
23
21
|
|
|
24
22
|
const saveConfig = async (conf: Config, _id?: string, _rev?: string) => {
|
|
25
|
-
const data = {
|
|
26
|
-
...conf,
|
|
27
|
-
_id,
|
|
28
|
-
_rev,
|
|
29
|
-
}
|
|
23
|
+
const data = { ...conf, _id, _rev }
|
|
30
24
|
const res = await config.api.configs.saveConfig(data)
|
|
31
25
|
return { ...data, ...res }
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
const saveSettingsConfig = async (
|
|
35
|
-
conf?: any,
|
|
36
|
-
_id?: string,
|
|
37
|
-
_rev?: string
|
|
38
|
-
) => {
|
|
39
|
-
const settingsConfig = structures.configs.settings(conf)
|
|
40
|
-
return saveConfig(settingsConfig, _id, _rev)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
28
|
describe("POST /api/global/configs", () => {
|
|
44
29
|
describe("google", () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
_rev?: string
|
|
49
|
-
) => {
|
|
50
|
-
const googleConfig = structures.configs.google(conf)
|
|
51
|
-
return saveConfig(googleConfig, _id, _rev)
|
|
52
|
-
}
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await config.deleteConfig(ConfigType.GOOGLE)
|
|
32
|
+
})
|
|
53
33
|
|
|
54
34
|
describe("create", () => {
|
|
55
35
|
it("should create activated google config", async () => {
|
|
56
|
-
await
|
|
36
|
+
await saveConfig(google())
|
|
57
37
|
expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
|
|
58
38
|
expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.GOOGLE)
|
|
59
39
|
expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
|
|
@@ -61,25 +41,23 @@ describe("configs", () => {
|
|
|
61
41
|
expect(events.auth.SSOActivated).toHaveBeenCalledWith(
|
|
62
42
|
ConfigType.GOOGLE
|
|
63
43
|
)
|
|
64
|
-
await config.deleteConfig(ConfigType.GOOGLE)
|
|
65
44
|
})
|
|
66
45
|
|
|
67
46
|
it("should create deactivated google config", async () => {
|
|
68
|
-
await
|
|
47
|
+
await saveConfig(google({ activated: false }))
|
|
69
48
|
expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
|
|
70
49
|
expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.GOOGLE)
|
|
71
50
|
expect(events.auth.SSOActivated).not.toHaveBeenCalled()
|
|
72
51
|
expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
|
|
73
|
-
await config.deleteConfig(ConfigType.GOOGLE)
|
|
74
52
|
})
|
|
75
53
|
})
|
|
76
54
|
|
|
77
55
|
describe("update", () => {
|
|
78
56
|
it("should update google config to deactivated", async () => {
|
|
79
|
-
const googleConf = await
|
|
57
|
+
const googleConf = await saveConfig(google())
|
|
80
58
|
jest.clearAllMocks()
|
|
81
|
-
await
|
|
82
|
-
{
|
|
59
|
+
await saveConfig(
|
|
60
|
+
google({ activated: false }),
|
|
83
61
|
googleConf._id,
|
|
84
62
|
googleConf._rev
|
|
85
63
|
)
|
|
@@ -90,14 +68,13 @@ describe("configs", () => {
|
|
|
90
68
|
expect(events.auth.SSODeactivated).toHaveBeenCalledWith(
|
|
91
69
|
ConfigType.GOOGLE
|
|
92
70
|
)
|
|
93
|
-
await config.deleteConfig(ConfigType.GOOGLE)
|
|
94
71
|
})
|
|
95
72
|
|
|
96
73
|
it("should update google config to activated", async () => {
|
|
97
|
-
const googleConf = await
|
|
74
|
+
const googleConf = await saveConfig(google({ activated: false }))
|
|
98
75
|
jest.clearAllMocks()
|
|
99
|
-
await
|
|
100
|
-
{
|
|
76
|
+
await saveConfig(
|
|
77
|
+
google({ activated: true }),
|
|
101
78
|
googleConf._id,
|
|
102
79
|
googleConf._rev
|
|
103
80
|
)
|
|
@@ -108,48 +85,64 @@ describe("configs", () => {
|
|
|
108
85
|
expect(events.auth.SSOActivated).toHaveBeenCalledWith(
|
|
109
86
|
ConfigType.GOOGLE
|
|
110
87
|
)
|
|
111
|
-
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("should not overwrite secret when updating google config", async () => {
|
|
91
|
+
await saveConfig(google({ clientSecret: "spooky" }))
|
|
92
|
+
|
|
93
|
+
const conf = await config.api.configs.getConfig(ConfigType.GOOGLE)
|
|
94
|
+
await saveConfig(conf)
|
|
95
|
+
|
|
96
|
+
await config.doInTenant(async () => {
|
|
97
|
+
const rawConf = await configs.getGoogleConfig()
|
|
98
|
+
expect(rawConf!.clientSecret).toEqual("spooky")
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe("get", () => {
|
|
104
|
+
it("should not leak credentials", async () => {
|
|
105
|
+
await saveConfig(google())
|
|
106
|
+
const conf = await config.api.configs.getConfig(ConfigType.GOOGLE)
|
|
107
|
+
expect(conf.config.clientSecret).toEqual("--secret-value--")
|
|
112
108
|
})
|
|
113
109
|
})
|
|
114
110
|
})
|
|
115
111
|
|
|
116
112
|
describe("oidc", () => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
113
|
+
beforeEach(async () => {
|
|
114
|
+
await config.deleteConfig(ConfigType.OIDC)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
afterEach(async () => {
|
|
118
|
+
await config.deleteConfig(ConfigType.OIDC)
|
|
119
|
+
})
|
|
125
120
|
|
|
126
121
|
describe("create", () => {
|
|
127
122
|
it("should create activated OIDC config", async () => {
|
|
128
|
-
await
|
|
123
|
+
await saveConfig(oidc())
|
|
129
124
|
expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
|
|
130
125
|
expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.OIDC)
|
|
131
126
|
expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
|
|
132
127
|
expect(events.auth.SSOActivated).toHaveBeenCalledTimes(1)
|
|
133
128
|
expect(events.auth.SSOActivated).toHaveBeenCalledWith(ConfigType.OIDC)
|
|
134
|
-
await config.deleteConfig(ConfigType.OIDC)
|
|
135
129
|
})
|
|
136
130
|
|
|
137
131
|
it("should create deactivated OIDC config", async () => {
|
|
138
|
-
await
|
|
132
|
+
await saveConfig(oidc({ activated: false }))
|
|
139
133
|
expect(events.auth.SSOCreated).toHaveBeenCalledTimes(1)
|
|
140
134
|
expect(events.auth.SSOCreated).toHaveBeenCalledWith(ConfigType.OIDC)
|
|
141
135
|
expect(events.auth.SSOActivated).not.toHaveBeenCalled()
|
|
142
136
|
expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
|
|
143
|
-
await config.deleteConfig(ConfigType.OIDC)
|
|
144
137
|
})
|
|
145
138
|
})
|
|
146
139
|
|
|
147
140
|
describe("update", () => {
|
|
148
141
|
it("should update OIDC config to deactivated", async () => {
|
|
149
|
-
const oidcConf = await
|
|
142
|
+
const oidcConf = await saveConfig(oidc())
|
|
150
143
|
jest.clearAllMocks()
|
|
151
|
-
await
|
|
152
|
-
{
|
|
144
|
+
await saveConfig(
|
|
145
|
+
oidc({ activated: false }),
|
|
153
146
|
oidcConf._id,
|
|
154
147
|
oidcConf._rev
|
|
155
148
|
)
|
|
@@ -160,14 +153,13 @@ describe("configs", () => {
|
|
|
160
153
|
expect(events.auth.SSODeactivated).toHaveBeenCalledWith(
|
|
161
154
|
ConfigType.OIDC
|
|
162
155
|
)
|
|
163
|
-
await config.deleteConfig(ConfigType.OIDC)
|
|
164
156
|
})
|
|
165
157
|
|
|
166
158
|
it("should update OIDC config to activated", async () => {
|
|
167
|
-
const oidcConf = await
|
|
159
|
+
const oidcConf = await saveConfig(oidc({ activated: false }))
|
|
168
160
|
jest.clearAllMocks()
|
|
169
|
-
await
|
|
170
|
-
{
|
|
161
|
+
await saveConfig(
|
|
162
|
+
oidc({ activated: true }),
|
|
171
163
|
oidcConf._id,
|
|
172
164
|
oidcConf._rev
|
|
173
165
|
)
|
|
@@ -176,50 +168,88 @@ describe("configs", () => {
|
|
|
176
168
|
expect(events.auth.SSODeactivated).not.toHaveBeenCalled()
|
|
177
169
|
expect(events.auth.SSOActivated).toHaveBeenCalledTimes(1)
|
|
178
170
|
expect(events.auth.SSOActivated).toHaveBeenCalledWith(ConfigType.OIDC)
|
|
179
|
-
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("should not overwrite secret when updating OIDC config", async () => {
|
|
174
|
+
await saveConfig(oidc({ clientSecret: "spooky" }))
|
|
175
|
+
const conf = await config.api.configs.getConfig(ConfigType.OIDC)
|
|
176
|
+
await saveConfig(conf)
|
|
177
|
+
await config.doInTenant(async () => {
|
|
178
|
+
const rawConf = await configs.getOIDCConfig()
|
|
179
|
+
expect(rawConf!.clientSecret).toEqual("spooky")
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe("get", () => {
|
|
185
|
+
it("should not leak credentials", async () => {
|
|
186
|
+
await saveConfig(oidc({ clientSecret: "spooky" }))
|
|
187
|
+
const conf = await config.api.configs.getConfig(ConfigType.OIDC)
|
|
188
|
+
expect(conf.config.configs[0].clientSecret).toEqual(
|
|
189
|
+
"--secret-value--"
|
|
190
|
+
)
|
|
180
191
|
})
|
|
181
192
|
})
|
|
182
193
|
})
|
|
183
194
|
|
|
184
195
|
describe("smtp", () => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
196
|
+
beforeEach(async () => {
|
|
197
|
+
await config.deleteConfig(ConfigType.SMTP)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
afterEach(async () => {
|
|
201
|
+
await config.deleteConfig(ConfigType.SMTP)
|
|
202
|
+
})
|
|
193
203
|
|
|
194
204
|
describe("create", () => {
|
|
195
205
|
it("should create SMTP config", async () => {
|
|
196
|
-
await
|
|
197
|
-
await saveSMTPConfig()
|
|
206
|
+
await saveConfig(smtp())
|
|
198
207
|
expect(events.email.SMTPUpdated).not.toHaveBeenCalled()
|
|
199
208
|
expect(events.email.SMTPCreated).toHaveBeenCalledTimes(1)
|
|
200
|
-
await config.deleteConfig(ConfigType.SMTP)
|
|
201
209
|
})
|
|
202
210
|
})
|
|
203
211
|
|
|
204
212
|
describe("update", () => {
|
|
205
213
|
it("should update SMTP config", async () => {
|
|
206
|
-
const smtpConf = await
|
|
214
|
+
const smtpConf = await saveConfig(smtp())
|
|
207
215
|
jest.clearAllMocks()
|
|
208
|
-
await
|
|
216
|
+
await saveConfig(smtp({ secure: true }), smtpConf._id, smtpConf._rev)
|
|
209
217
|
expect(events.email.SMTPCreated).not.toHaveBeenCalled()
|
|
210
218
|
expect(events.email.SMTPUpdated).toHaveBeenCalledTimes(1)
|
|
211
|
-
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should not overwrite secret when updating SMTP config", async () => {
|
|
222
|
+
await saveConfig(smtp({ auth: { user: "jeff", pass: "spooky" } }))
|
|
223
|
+
const conf = await config.api.configs.getConfig(ConfigType.SMTP)
|
|
224
|
+
await saveConfig(conf)
|
|
225
|
+
await config.doInTenant(async () => {
|
|
226
|
+
const rawConf = await configs.getSMTPConfig()
|
|
227
|
+
expect(rawConf!.auth!.pass).toEqual("spooky")
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe("get", () => {
|
|
233
|
+
it("should not leak credentials", async () => {
|
|
234
|
+
await saveConfig(smtp({ auth: { user: "jeff", pass: "spooky" } }))
|
|
235
|
+
const conf = await config.api.configs.getConfig(ConfigType.SMTP)
|
|
236
|
+
expect(conf.config.auth!.pass).toEqual("--secret-value--")
|
|
212
237
|
})
|
|
213
238
|
})
|
|
214
239
|
})
|
|
215
240
|
|
|
216
241
|
describe("settings", () => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
beforeEach(async () => {
|
|
243
|
+
await config.deleteConfig(ConfigType.SETTINGS)
|
|
244
|
+
})
|
|
220
245
|
|
|
221
|
-
|
|
246
|
+
afterEach(async () => {
|
|
247
|
+
await config.deleteConfig(ConfigType.SETTINGS)
|
|
248
|
+
})
|
|
222
249
|
|
|
250
|
+
describe("create", () => {
|
|
251
|
+
it("should create settings config with default settings", async () => {
|
|
252
|
+
await saveConfig(settings())
|
|
223
253
|
expect(events.org.nameUpdated).not.toHaveBeenCalled()
|
|
224
254
|
expect(events.org.logoUpdated).not.toHaveBeenCalled()
|
|
225
255
|
expect(events.org.platformURLUpdated).not.toHaveBeenCalled()
|
|
@@ -234,7 +264,7 @@ describe("configs", () => {
|
|
|
234
264
|
platformUrl: "http://example.com",
|
|
235
265
|
}
|
|
236
266
|
|
|
237
|
-
await
|
|
267
|
+
await saveConfig(settings(conf))
|
|
238
268
|
|
|
239
269
|
expect(events.org.nameUpdated).toHaveBeenCalledTimes(1)
|
|
240
270
|
expect(events.org.logoUpdated).toHaveBeenCalledTimes(1)
|
|
@@ -247,13 +277,13 @@ describe("configs", () => {
|
|
|
247
277
|
it("should update settings config", async () => {
|
|
248
278
|
config.selfHosted()
|
|
249
279
|
await config.deleteConfig(ConfigType.SETTINGS)
|
|
250
|
-
const settingsConfig = await
|
|
280
|
+
const settingsConfig = await saveConfig(settings())
|
|
251
281
|
settingsConfig.config.company = "acme"
|
|
252
282
|
settingsConfig.config.logoUrl = "http://example.com"
|
|
253
283
|
settingsConfig.config.platformUrl = "http://example.com"
|
|
254
284
|
|
|
255
|
-
await
|
|
256
|
-
settingsConfig
|
|
285
|
+
await saveConfig(
|
|
286
|
+
settingsConfig,
|
|
257
287
|
settingsConfig._id,
|
|
258
288
|
settingsConfig._rev
|
|
259
289
|
)
|
|
@@ -282,16 +312,24 @@ describe("configs", () => {
|
|
|
282
312
|
})
|
|
283
313
|
|
|
284
314
|
describe("GET /api/global/configs/public", () => {
|
|
315
|
+
beforeEach(async () => {
|
|
316
|
+
await config.deleteConfig(ConfigType.SETTINGS)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
afterEach(async () => {
|
|
320
|
+
await config.deleteConfig(ConfigType.SETTINGS)
|
|
321
|
+
})
|
|
322
|
+
|
|
285
323
|
it("should return the expected public settings", async () => {
|
|
286
|
-
await
|
|
324
|
+
await saveConfig(settings())
|
|
287
325
|
mocks.pro.features.isSSOEnforced.mockResolvedValue(false)
|
|
288
326
|
|
|
289
327
|
const res = await config.api.configs.getPublicSettings()
|
|
290
328
|
const body = res.body as GetPublicSettingsResponse
|
|
291
329
|
|
|
292
330
|
const expected = {
|
|
293
|
-
_id:
|
|
294
|
-
type:
|
|
331
|
+
_id: `config_${ConfigType.SETTINGS}`,
|
|
332
|
+
type: ConfigType.SETTINGS,
|
|
295
333
|
config: {
|
|
296
334
|
company: "Budibase",
|
|
297
335
|
emailBrandingEnabled: true,
|
|
@@ -704,6 +704,24 @@ describe("/api/global/users", () => {
|
|
|
704
704
|
expect(response.body.data.length).toBe(1)
|
|
705
705
|
expect(response.body.data[0].email).toBe(user.email)
|
|
706
706
|
})
|
|
707
|
+
|
|
708
|
+
it("should strip users if accessing as an end user", async () => {
|
|
709
|
+
const user = await config.createUser({
|
|
710
|
+
admin: { global: false },
|
|
711
|
+
builder: { global: false },
|
|
712
|
+
})
|
|
713
|
+
const response = await config.api.users.searchUsers(
|
|
714
|
+
{
|
|
715
|
+
query: {},
|
|
716
|
+
},
|
|
717
|
+
{ useHeaders: await config.login(user) }
|
|
718
|
+
)
|
|
719
|
+
for (let user of response.body.data) {
|
|
720
|
+
expect(user.roles).toBeUndefined()
|
|
721
|
+
expect(user.builder).toBeUndefined()
|
|
722
|
+
expect(user.admin).toBeUndefined()
|
|
723
|
+
}
|
|
724
|
+
})
|
|
707
725
|
})
|
|
708
726
|
|
|
709
727
|
describe("DELETE /api/global/users/:userId", () => {
|
|
@@ -46,6 +46,7 @@ class TestConfiguration {
|
|
|
46
46
|
user?: User
|
|
47
47
|
apiKey?: string
|
|
48
48
|
userPassword = "password123!"
|
|
49
|
+
sessions: string[] = []
|
|
49
50
|
|
|
50
51
|
constructor(opts: { openServer: boolean } = { openServer: true }) {
|
|
51
52
|
// default to cloud hosting
|
|
@@ -185,12 +186,19 @@ class TestConfiguration {
|
|
|
185
186
|
})
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
hasSession(user: User) {
|
|
190
|
+
return this.sessions.includes(user._id!)
|
|
191
|
+
}
|
|
192
|
+
|
|
188
193
|
async createSession(user: User) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
if (!this.hasSession(user)) {
|
|
195
|
+
this.sessions.push(user._id!)
|
|
196
|
+
return this._createSession({
|
|
197
|
+
userId: user._id!,
|
|
198
|
+
tenantId: user.tenantId,
|
|
199
|
+
email: user.email,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
cookieHeader(cookies: any) {
|
|
@@ -212,6 +220,11 @@ class TestConfiguration {
|
|
|
212
220
|
}
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
async login(user: User) {
|
|
224
|
+
await this.createSession(user)
|
|
225
|
+
return this.authHeaders(user)
|
|
226
|
+
}
|
|
227
|
+
|
|
215
228
|
authHeaders(user: User) {
|
|
216
229
|
const authToken: AuthToken = {
|
|
217
230
|
userId: user._id!,
|
|
@@ -279,10 +292,10 @@ class TestConfiguration {
|
|
|
279
292
|
})
|
|
280
293
|
}
|
|
281
294
|
|
|
282
|
-
async createUser(
|
|
295
|
+
async createUser(userCfg?: Partial<User>) {
|
|
283
296
|
let user = structures.users.user()
|
|
284
297
|
if (user) {
|
|
285
|
-
user = { ...user, ...
|
|
298
|
+
user = { ...user, ...userCfg }
|
|
286
299
|
}
|
|
287
300
|
const response = await this._req(user, null, controllers.users.save)
|
|
288
301
|
const body = response as SaveUserResponse
|
package/src/tests/api/configs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
ConfigType,
|
|
3
|
+
ConfigTypeToConfig,
|
|
3
4
|
SaveConfigRequest,
|
|
4
5
|
SaveConfigResponse,
|
|
5
6
|
} from "@budibase/types"
|
|
@@ -23,12 +24,16 @@ export class ConfigAPI extends TestAPI {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
getAIConfig = async () => {
|
|
27
|
+
return await this.getConfig(ConfigType.AI)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getConfig = async <T extends ConfigType>(type: T) => {
|
|
26
31
|
const resp = await this.request
|
|
27
|
-
.get(`/api/global/configs
|
|
32
|
+
.get(`/api/global/configs/${type}`)
|
|
28
33
|
.set(this.config.defaultHeaders())
|
|
29
34
|
.expect(200)
|
|
30
35
|
.expect("Content-Type", /json/)
|
|
31
|
-
return resp.body as
|
|
36
|
+
return resp.body as ConfigTypeToConfig<T>
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
saveConfig = async (
|
package/src/tests/api/users.ts
CHANGED
|
@@ -152,14 +152,20 @@ export class UserAPI extends TestAPI {
|
|
|
152
152
|
|
|
153
153
|
searchUsers = (
|
|
154
154
|
{ query }: { query?: SearchFilters },
|
|
155
|
-
opts?: {
|
|
155
|
+
opts?: {
|
|
156
|
+
status?: number
|
|
157
|
+
noHeaders?: boolean
|
|
158
|
+
useHeaders?: Record<string, string>
|
|
159
|
+
}
|
|
156
160
|
) => {
|
|
157
161
|
const req = this.request
|
|
158
162
|
.post("/api/global/users/search")
|
|
159
163
|
.send({ query })
|
|
160
164
|
.expect("Content-Type", /json/)
|
|
161
165
|
.expect(opts?.status ? opts.status : 200)
|
|
162
|
-
if (
|
|
166
|
+
if (opts?.useHeaders) {
|
|
167
|
+
req.set(opts.useHeaders)
|
|
168
|
+
} else if (!opts?.noHeaders) {
|
|
163
169
|
req.set(this.config.defaultHeaders())
|
|
164
170
|
}
|
|
165
171
|
return req
|
|
@@ -5,9 +5,13 @@ import {
|
|
|
5
5
|
SMTPConfig,
|
|
6
6
|
GoogleConfig,
|
|
7
7
|
OIDCConfig,
|
|
8
|
+
GoogleInnerConfig,
|
|
9
|
+
OIDCInnerConfig,
|
|
10
|
+
SMTPInnerConfig,
|
|
11
|
+
SettingsInnerConfig,
|
|
8
12
|
} from "@budibase/types"
|
|
9
13
|
|
|
10
|
-
export function oidc(conf?:
|
|
14
|
+
export function oidc(conf?: Partial<OIDCInnerConfig>): OIDCConfig {
|
|
11
15
|
return {
|
|
12
16
|
type: ConfigType.OIDC,
|
|
13
17
|
config: {
|
|
@@ -20,6 +24,7 @@ export function oidc(conf?: any): OIDCConfig {
|
|
|
20
24
|
name: "Active Directory",
|
|
21
25
|
uuid: utils.newid(),
|
|
22
26
|
activated: true,
|
|
27
|
+
scopes: [],
|
|
23
28
|
...conf,
|
|
24
29
|
},
|
|
25
30
|
],
|
|
@@ -27,7 +32,7 @@ export function oidc(conf?: any): OIDCConfig {
|
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export function google(conf?:
|
|
35
|
+
export function google(conf?: Partial<GoogleInnerConfig>): GoogleConfig {
|
|
31
36
|
return {
|
|
32
37
|
type: ConfigType.GOOGLE,
|
|
33
38
|
config: {
|
|
@@ -39,7 +44,7 @@ export function google(conf?: any): GoogleConfig {
|
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
export function smtp(conf?:
|
|
47
|
+
export function smtp(conf?: Partial<SMTPInnerConfig>): SMTPConfig {
|
|
43
48
|
return {
|
|
44
49
|
type: ConfigType.SMTP,
|
|
45
50
|
config: {
|
|
@@ -70,7 +75,7 @@ export function smtpEthereal(): SMTPConfig {
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
export function settings(conf?:
|
|
78
|
+
export function settings(conf?: Partial<SettingsInnerConfig>): SettingsConfig {
|
|
74
79
|
return {
|
|
75
80
|
type: ConfigType.SETTINGS,
|
|
76
81
|
config: {
|