@budibase/worker 2.3.18-alpha.12 → 2.3.18-alpha.13
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 +6 -6
- package/src/api/controllers/global/auth.ts +32 -41
- package/src/api/controllers/global/configs.ts +82 -143
- package/src/api/routes/global/configs.ts +0 -6
- package/src/api/routes/global/tests/auth.spec.ts +2 -2
- package/src/api/routes/global/tests/configs.spec.ts +88 -60
- package/src/api/routes/global/tests/realEmail.spec.ts +1 -0
- package/src/environment.ts +1 -11
- package/src/index.ts +8 -2
- package/src/sdk/auth/auth.ts +2 -2
- package/src/sdk/users/tests/users.spec.ts +31 -7
- package/src/sdk/users/users.ts +13 -7
- package/src/tests/api/configs.ts +8 -0
- package/src/tests/api/email.ts +1 -0
- package/src/tests/structures/configs.ts +20 -13
- package/src/utilities/email.ts +18 -64
- package/src/utilities/templates.ts +4 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "2.3.18-alpha.
|
|
4
|
+
"version": "2.3.18-alpha.13",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"author": "Budibase",
|
|
37
37
|
"license": "GPL-3.0",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@budibase/backend-core": "2.3.18-alpha.
|
|
40
|
-
"@budibase/pro": "2.3.18-alpha.
|
|
41
|
-
"@budibase/string-templates": "2.3.18-alpha.
|
|
42
|
-
"@budibase/types": "2.3.18-alpha.
|
|
39
|
+
"@budibase/backend-core": "2.3.18-alpha.13",
|
|
40
|
+
"@budibase/pro": "2.3.18-alpha.12",
|
|
41
|
+
"@budibase/string-templates": "2.3.18-alpha.13",
|
|
42
|
+
"@budibase/types": "2.3.18-alpha.13",
|
|
43
43
|
"@koa/router": "8.0.8",
|
|
44
44
|
"@sentry/node": "6.17.7",
|
|
45
45
|
"@techpass/passport-openidconnect": "0.3.2",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"typescript": "4.7.3",
|
|
101
101
|
"update-dotenv": "1.1.1"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "95ce3bb56c8db6b69b3b9081c0eeb0fff5180518"
|
|
104
104
|
}
|
|
@@ -2,10 +2,9 @@ import {
|
|
|
2
2
|
auth as authCore,
|
|
3
3
|
constants,
|
|
4
4
|
context,
|
|
5
|
-
db as dbCore,
|
|
6
5
|
events,
|
|
7
|
-
tenancy,
|
|
8
6
|
utils as utilsCore,
|
|
7
|
+
configs,
|
|
9
8
|
} from "@budibase/backend-core"
|
|
10
9
|
import {
|
|
11
10
|
ConfigType,
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
SSOUser,
|
|
16
15
|
PasswordResetRequest,
|
|
17
16
|
PasswordResetUpdateRequest,
|
|
17
|
+
GoogleInnerConfig,
|
|
18
18
|
} from "@budibase/types"
|
|
19
19
|
import env from "../../../environment"
|
|
20
20
|
|
|
@@ -61,8 +61,8 @@ export const login = async (ctx: Ctx<LoginRequest>, next: any) => {
|
|
|
61
61
|
const email = ctx.request.body.username
|
|
62
62
|
|
|
63
63
|
const user = await userSdk.getUserByEmail(email)
|
|
64
|
-
if (user && (await userSdk.
|
|
65
|
-
ctx.throw(400, "
|
|
64
|
+
if (user && (await userSdk.isPreventPasswordActions(user))) {
|
|
65
|
+
ctx.throw(400, "Password login is disabled for this user")
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return passport.authenticate(
|
|
@@ -163,8 +163,8 @@ export const datasourceAuth = async (ctx: any, next: any) => {
|
|
|
163
163
|
|
|
164
164
|
// GOOGLE SSO
|
|
165
165
|
|
|
166
|
-
export async function googleCallbackUrl(config?:
|
|
167
|
-
return ssoCallbackUrl(
|
|
166
|
+
export async function googleCallbackUrl(config?: GoogleInnerConfig) {
|
|
167
|
+
return ssoCallbackUrl(ConfigType.GOOGLE, config)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
@@ -172,12 +172,10 @@ export async function googleCallbackUrl(config?: { callbackURL?: string }) {
|
|
|
172
172
|
* On a successful login, you will be redirected to the googleAuth callback route.
|
|
173
173
|
*/
|
|
174
174
|
export const googlePreAuth = async (ctx: any, next: any) => {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
workspace: ctx.query.workspace,
|
|
180
|
-
})
|
|
175
|
+
const config = await configs.getGoogleConfig()
|
|
176
|
+
if (!config) {
|
|
177
|
+
return ctx.throw(400, "Google config not found")
|
|
178
|
+
}
|
|
181
179
|
let callbackUrl = await googleCallbackUrl(config)
|
|
182
180
|
const strategy = await google.strategyFactory(
|
|
183
181
|
config,
|
|
@@ -193,12 +191,10 @@ export const googlePreAuth = async (ctx: any, next: any) => {
|
|
|
193
191
|
}
|
|
194
192
|
|
|
195
193
|
export const googleCallback = async (ctx: any, next: any) => {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
workspace: ctx.query.workspace,
|
|
201
|
-
})
|
|
194
|
+
const config = await configs.getGoogleConfig()
|
|
195
|
+
if (!config) {
|
|
196
|
+
return ctx.throw(400, "Google config not found")
|
|
197
|
+
}
|
|
202
198
|
const callbackUrl = await googleCallbackUrl(config)
|
|
203
199
|
const strategy = await google.strategyFactory(
|
|
204
200
|
config,
|
|
@@ -221,25 +217,20 @@ export const googleCallback = async (ctx: any, next: any) => {
|
|
|
221
217
|
|
|
222
218
|
// OIDC SSO
|
|
223
219
|
|
|
224
|
-
export async function oidcCallbackUrl(
|
|
225
|
-
return ssoCallbackUrl(
|
|
220
|
+
export async function oidcCallbackUrl() {
|
|
221
|
+
return ssoCallbackUrl(ConfigType.OIDC)
|
|
226
222
|
}
|
|
227
223
|
|
|
228
224
|
export const oidcStrategyFactory = async (ctx: any, configId: any) => {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
})
|
|
225
|
+
const config = await configs.getOIDCConfig()
|
|
226
|
+
if (!config) {
|
|
227
|
+
return ctx.throw(400, "OIDC config not found")
|
|
228
|
+
}
|
|
234
229
|
|
|
235
|
-
|
|
236
|
-
let callbackUrl = await oidcCallbackUrl(chosenConfig)
|
|
230
|
+
let callbackUrl = await oidcCallbackUrl()
|
|
237
231
|
|
|
238
232
|
//Remote Config
|
|
239
|
-
const enrichedConfig = await oidc.fetchStrategyConfig(
|
|
240
|
-
chosenConfig,
|
|
241
|
-
callbackUrl
|
|
242
|
-
)
|
|
233
|
+
const enrichedConfig = await oidc.fetchStrategyConfig(config, callbackUrl)
|
|
243
234
|
return oidc.strategyFactory(enrichedConfig, userSdk.save)
|
|
244
235
|
}
|
|
245
236
|
|
|
@@ -247,23 +238,23 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => {
|
|
|
247
238
|
* The initial call that OIDC authentication makes to take you to the configured OIDC login screen.
|
|
248
239
|
* On a successful login, you will be redirected to the oidcAuth callback route.
|
|
249
240
|
*/
|
|
250
|
-
export const oidcPreAuth = async (ctx:
|
|
241
|
+
export const oidcPreAuth = async (ctx: Ctx, next: any) => {
|
|
251
242
|
const { configId } = ctx.params
|
|
243
|
+
if (!configId) {
|
|
244
|
+
ctx.throw(400, "OIDC config id is required")
|
|
245
|
+
}
|
|
252
246
|
const strategy = await oidcStrategyFactory(ctx, configId)
|
|
253
247
|
|
|
254
248
|
setCookie(ctx, configId, Cookie.OIDC_CONFIG)
|
|
255
249
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
|
250
|
+
const config = await configs.getOIDCConfigById(configId)
|
|
251
|
+
if (!config) {
|
|
252
|
+
return ctx.throw(400, "OIDC config not found")
|
|
253
|
+
}
|
|
263
254
|
|
|
264
255
|
let authScopes =
|
|
265
|
-
|
|
266
|
-
?
|
|
256
|
+
config.scopes?.length > 0
|
|
257
|
+
? config.scopes
|
|
267
258
|
: ["profile", "email", "offline_access"]
|
|
268
259
|
|
|
269
260
|
return passport.authenticate(strategy, {
|
|
@@ -2,38 +2,32 @@ import * as email from "../../../utilities/email"
|
|
|
2
2
|
import env from "../../../environment"
|
|
3
3
|
import { googleCallbackUrl, oidcCallbackUrl } from "./auth"
|
|
4
4
|
import {
|
|
5
|
-
events,
|
|
6
5
|
cache,
|
|
7
|
-
|
|
8
|
-
tenancy,
|
|
6
|
+
configs,
|
|
9
7
|
db as dbCore,
|
|
10
8
|
env as coreEnv,
|
|
9
|
+
events,
|
|
10
|
+
objectStore,
|
|
11
|
+
tenancy,
|
|
11
12
|
} from "@budibase/backend-core"
|
|
12
13
|
import { checkAnyUserExists } from "../../../utilities/users"
|
|
13
14
|
import {
|
|
14
|
-
|
|
15
|
-
Config as ConfigDoc,
|
|
15
|
+
Config,
|
|
16
16
|
ConfigType,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
SettingsConfig,
|
|
17
|
+
Ctx,
|
|
18
|
+
GetPublicOIDCConfigResponse,
|
|
19
|
+
GetPublicSettingsResponse,
|
|
21
20
|
isGoogleConfig,
|
|
22
21
|
isOIDCConfig,
|
|
23
22
|
isSettingsConfig,
|
|
24
23
|
isSMTPConfig,
|
|
25
|
-
Ctx,
|
|
26
24
|
UserCtx,
|
|
27
25
|
} from "@budibase/types"
|
|
26
|
+
import * as pro from "@budibase/pro"
|
|
28
27
|
|
|
29
|
-
const getEventFns = async (
|
|
28
|
+
const getEventFns = async (config: Config, existing?: Config) => {
|
|
30
29
|
const fns = []
|
|
31
30
|
|
|
32
|
-
let existing
|
|
33
|
-
if (config._id) {
|
|
34
|
-
existing = await db.get(config._id)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
31
|
if (!existing) {
|
|
38
32
|
if (isSMTPConfig(config)) {
|
|
39
33
|
fns.push(events.email.SMTPCreated)
|
|
@@ -125,21 +119,21 @@ const getEventFns = async (db: Database, config: ConfigDoc) => {
|
|
|
125
119
|
return fns
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
export async function save(ctx: UserCtx) {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
})
|
|
122
|
+
export async function save(ctx: UserCtx<Config>) {
|
|
123
|
+
const body = ctx.request.body
|
|
124
|
+
const type = body.type
|
|
125
|
+
const config = body.config
|
|
126
|
+
|
|
127
|
+
const existingConfig = await configs.getConfig(type)
|
|
128
|
+
let eventFns = await getEventFns(ctx.request.body, existingConfig)
|
|
129
|
+
|
|
130
|
+
if (existingConfig) {
|
|
131
|
+
body._rev = existingConfig._rev
|
|
139
132
|
}
|
|
133
|
+
|
|
140
134
|
try {
|
|
141
135
|
// verify the configuration
|
|
142
|
-
switch (type) {
|
|
136
|
+
switch (config.type) {
|
|
143
137
|
case ConfigType.SMTP:
|
|
144
138
|
await email.verifyConfig(config)
|
|
145
139
|
break
|
|
@@ -149,7 +143,8 @@ export async function save(ctx: UserCtx) {
|
|
|
149
143
|
}
|
|
150
144
|
|
|
151
145
|
try {
|
|
152
|
-
|
|
146
|
+
body._id = configs.generateConfigID(type)
|
|
147
|
+
const response = await configs.save(body)
|
|
153
148
|
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
|
154
149
|
await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED)
|
|
155
150
|
|
|
@@ -167,44 +162,11 @@ export async function save(ctx: UserCtx) {
|
|
|
167
162
|
}
|
|
168
163
|
}
|
|
169
164
|
|
|
170
|
-
export async function fetch(ctx: UserCtx) {
|
|
171
|
-
const db = tenancy.getGlobalDB()
|
|
172
|
-
const response = await db.allDocs(
|
|
173
|
-
dbCore.getConfigParams(
|
|
174
|
-
{ type: ctx.params.type },
|
|
175
|
-
{
|
|
176
|
-
include_docs: true,
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
)
|
|
180
|
-
ctx.body = response.rows.map(row => row.doc)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Gets the most granular config for a particular configuration type.
|
|
185
|
-
* The hierarchy is type -> workspace -> user.
|
|
186
|
-
*/
|
|
187
165
|
export async function find(ctx: UserCtx) {
|
|
188
|
-
const db = tenancy.getGlobalDB()
|
|
189
|
-
|
|
190
|
-
const { userId, workspaceId } = ctx.query
|
|
191
|
-
if (workspaceId && userId) {
|
|
192
|
-
const workspace = await db.get(workspaceId as string)
|
|
193
|
-
const userInWorkspace = workspace.users.some(
|
|
194
|
-
(workspaceUser: any) => workspaceUser === userId
|
|
195
|
-
)
|
|
196
|
-
if (!ctx.user!.admin && !userInWorkspace) {
|
|
197
|
-
ctx.throw(400, `User is not in specified workspace: ${workspace}.`)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
166
|
try {
|
|
202
167
|
// Find the config with the most granular scope based on context
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
user: userId,
|
|
206
|
-
workspace: workspaceId,
|
|
207
|
-
})
|
|
168
|
+
const type = ctx.params.type
|
|
169
|
+
const scopedConfig = await configs.getConfig(type)
|
|
208
170
|
|
|
209
171
|
if (scopedConfig) {
|
|
210
172
|
ctx.body = scopedConfig
|
|
@@ -217,85 +179,70 @@ export async function find(ctx: UserCtx) {
|
|
|
217
179
|
}
|
|
218
180
|
}
|
|
219
181
|
|
|
220
|
-
export async function publicOidc(ctx: Ctx) {
|
|
221
|
-
const db = tenancy.getGlobalDB()
|
|
182
|
+
export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
|
|
222
183
|
try {
|
|
223
184
|
// Find the config with the most granular scope based on context
|
|
224
|
-
const
|
|
225
|
-
type: ConfigType.OIDC,
|
|
226
|
-
})
|
|
185
|
+
const config = await configs.getOIDCConfig()
|
|
227
186
|
|
|
228
|
-
if (!
|
|
229
|
-
ctx.body =
|
|
187
|
+
if (!config) {
|
|
188
|
+
ctx.body = []
|
|
230
189
|
} else {
|
|
231
|
-
ctx.body =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
190
|
+
ctx.body = [
|
|
191
|
+
{
|
|
192
|
+
logo: config.logo,
|
|
193
|
+
name: config.name,
|
|
194
|
+
uuid: config.uuid,
|
|
195
|
+
},
|
|
196
|
+
]
|
|
236
197
|
}
|
|
237
198
|
} catch (err: any) {
|
|
238
199
|
ctx.throw(err.status, err)
|
|
239
200
|
}
|
|
240
201
|
}
|
|
241
202
|
|
|
242
|
-
export async function publicSettings(
|
|
243
|
-
|
|
244
|
-
|
|
203
|
+
export async function publicSettings(
|
|
204
|
+
ctx: Ctx<void, GetPublicSettingsResponse>
|
|
205
|
+
) {
|
|
245
206
|
try {
|
|
246
|
-
//
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
type: ConfigType.GOOGLE,
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
const oidcConfig = await dbCore.getScopedFullConfig(db, {
|
|
256
|
-
type: ConfigType.OIDC,
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
let config
|
|
260
|
-
if (!publicConfig) {
|
|
261
|
-
config = {
|
|
262
|
-
config: {},
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
config = publicConfig
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// enrich the logo url
|
|
269
|
-
// empty url means deleted
|
|
270
|
-
if (config.config.logoUrl && config.config.logoUrl !== "") {
|
|
271
|
-
config.config.logoUrl = objectStore.getGlobalFileUrl(
|
|
207
|
+
// settings
|
|
208
|
+
const configDoc = await configs.getSettingsConfigDoc()
|
|
209
|
+
const config = configDoc.config
|
|
210
|
+
// enrich the logo url - empty url means deleted
|
|
211
|
+
if (config.logoUrl && config.logoUrl !== "") {
|
|
212
|
+
config.logoUrl = objectStore.getGlobalFileUrl(
|
|
272
213
|
"settings",
|
|
273
214
|
"logoUrl",
|
|
274
|
-
config.
|
|
215
|
+
config.logoUrlEtag
|
|
275
216
|
)
|
|
276
217
|
}
|
|
277
218
|
|
|
278
|
-
// google
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
} else {
|
|
284
|
-
config.config.google = false
|
|
285
|
-
}
|
|
219
|
+
// google
|
|
220
|
+
const googleConfig = await configs.getGoogleConfig()
|
|
221
|
+
const preActivated = googleConfig?.activated == null
|
|
222
|
+
const google = preActivated || !!googleConfig?.activated
|
|
223
|
+
const _googleCallbackUrl = await googleCallbackUrl(googleConfig)
|
|
286
224
|
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
225
|
+
// oidc
|
|
226
|
+
const oidcConfig = await configs.getOIDCConfig()
|
|
227
|
+
const oidc = oidcConfig?.activated || false
|
|
228
|
+
const _oidcCallbackUrl = await oidcCallbackUrl()
|
|
290
229
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
config.config.oidc = oidcConfig.config.configs[0].activated
|
|
294
|
-
} else {
|
|
295
|
-
config.config.oidc = false
|
|
296
|
-
}
|
|
230
|
+
// sso enforced
|
|
231
|
+
const isSSOEnforced = await pro.features.isSSOEnforced({ config })
|
|
297
232
|
|
|
298
|
-
ctx.body =
|
|
233
|
+
ctx.body = {
|
|
234
|
+
type: ConfigType.SETTINGS,
|
|
235
|
+
_id: configDoc._id,
|
|
236
|
+
_rev: configDoc._rev,
|
|
237
|
+
config: {
|
|
238
|
+
...config,
|
|
239
|
+
google,
|
|
240
|
+
oidc,
|
|
241
|
+
isSSOEnforced,
|
|
242
|
+
oidcCallbackUrl: _oidcCallbackUrl,
|
|
243
|
+
googleCallbackUrl: _googleCallbackUrl,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
299
246
|
} catch (err: any) {
|
|
300
247
|
ctx.throw(err.status, err)
|
|
301
248
|
}
|
|
@@ -319,12 +266,11 @@ export async function upload(ctx: UserCtx) {
|
|
|
319
266
|
})
|
|
320
267
|
|
|
321
268
|
// add to configuration structure
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
_id: dbCore.generateConfigID({ type }),
|
|
269
|
+
let config = await configs.getConfig(type)
|
|
270
|
+
if (!config) {
|
|
271
|
+
config = {
|
|
272
|
+
_id: configs.generateConfigID(type),
|
|
273
|
+
type,
|
|
328
274
|
config: {},
|
|
329
275
|
}
|
|
330
276
|
}
|
|
@@ -332,14 +278,14 @@ export async function upload(ctx: UserCtx) {
|
|
|
332
278
|
// save the Etag for cache bursting
|
|
333
279
|
const etag = result.ETag
|
|
334
280
|
if (etag) {
|
|
335
|
-
|
|
281
|
+
config.config[`${name}Etag`] = etag.replace(/"/g, "")
|
|
336
282
|
}
|
|
337
283
|
|
|
338
284
|
// save the file key
|
|
339
|
-
|
|
285
|
+
config.config[`${name}`] = key
|
|
340
286
|
|
|
341
287
|
// write back to db
|
|
342
|
-
await
|
|
288
|
+
await configs.save(config)
|
|
343
289
|
|
|
344
290
|
ctx.body = {
|
|
345
291
|
message: "File has been uploaded and url stored to config.",
|
|
@@ -360,7 +306,6 @@ export async function destroy(ctx: UserCtx) {
|
|
|
360
306
|
}
|
|
361
307
|
|
|
362
308
|
export async function configChecklist(ctx: Ctx) {
|
|
363
|
-
const db = tenancy.getGlobalDB()
|
|
364
309
|
const tenantId = tenancy.getTenantId()
|
|
365
310
|
|
|
366
311
|
try {
|
|
@@ -375,19 +320,13 @@ export async function configChecklist(ctx: Ctx) {
|
|
|
375
320
|
}
|
|
376
321
|
|
|
377
322
|
// They have set up SMTP
|
|
378
|
-
const smtpConfig = await
|
|
379
|
-
type: ConfigType.SMTP,
|
|
380
|
-
})
|
|
323
|
+
const smtpConfig = await configs.getSMTPConfig()
|
|
381
324
|
|
|
382
325
|
// They have set up Google Auth
|
|
383
|
-
const googleConfig = await
|
|
384
|
-
type: ConfigType.GOOGLE,
|
|
385
|
-
})
|
|
326
|
+
const googleConfig = await configs.getGoogleConfig()
|
|
386
327
|
|
|
387
328
|
// They have set up OIDC
|
|
388
|
-
const oidcConfig = await
|
|
389
|
-
type: ConfigType.OIDC,
|
|
390
|
-
})
|
|
329
|
+
const oidcConfig = await configs.getOIDCConfig()
|
|
391
330
|
|
|
392
331
|
// They have set up a global user
|
|
393
332
|
const userExists = await checkAnyUserExists()
|
|
@@ -104,13 +104,7 @@ router
|
|
|
104
104
|
controller.save
|
|
105
105
|
)
|
|
106
106
|
.delete("/api/global/configs/:id/:rev", auth.adminOnly, controller.destroy)
|
|
107
|
-
.get("/api/global/configs", controller.fetch)
|
|
108
107
|
.get("/api/global/configs/checklist", controller.configChecklist)
|
|
109
|
-
.get(
|
|
110
|
-
"/api/global/configs/all/:type",
|
|
111
|
-
buildConfigGetValidation(),
|
|
112
|
-
controller.fetch
|
|
113
|
-
)
|
|
114
108
|
.get("/api/global/configs/public", controller.publicSettings)
|
|
115
109
|
.get("/api/global/configs/public/oidc", controller.publicOidc)
|
|
116
110
|
.get("/api/global/configs/:type", buildConfigGetValidation(), controller.find)
|
|
@@ -110,7 +110,7 @@ describe("/api/global/auth", () => {
|
|
|
110
110
|
)
|
|
111
111
|
|
|
112
112
|
expect(response.body).toEqual({
|
|
113
|
-
message: "
|
|
113
|
+
message: "Password login is disabled for this user",
|
|
114
114
|
status: 400,
|
|
115
115
|
})
|
|
116
116
|
}
|
|
@@ -175,7 +175,7 @@ describe("/api/global/auth", () => {
|
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
expect(res.body).toEqual({
|
|
178
|
-
message: "
|
|
178
|
+
message: "Password reset is disabled for this user",
|
|
179
179
|
status: 400,
|
|
180
180
|
error: {
|
|
181
181
|
code: "http",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
jest.mock("nodemailer")
|
|
3
3
|
import { TestConfiguration, structures, mocks } from "../../../../tests"
|
|
4
4
|
mocks.email.mock()
|
|
5
|
-
import {
|
|
5
|
+
import { events } from "@budibase/backend-core"
|
|
6
|
+
import { GetPublicSettingsResponse, Config, ConfigType } from "@budibase/types"
|
|
6
7
|
|
|
7
8
|
describe("configs", () => {
|
|
8
9
|
const config = new TestConfiguration()
|
|
@@ -19,22 +20,29 @@ describe("configs", () => {
|
|
|
19
20
|
await config.afterAll()
|
|
20
21
|
})
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
_rev,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const res = await config.api.configs.saveConfig(data)
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
...data,
|
|
34
|
-
...res.body,
|
|
35
|
-
}
|
|
23
|
+
const saveConfig = async (conf: Config, _id?: string, _rev?: string) => {
|
|
24
|
+
const data = {
|
|
25
|
+
...conf,
|
|
26
|
+
_id,
|
|
27
|
+
_rev,
|
|
36
28
|
}
|
|
37
|
-
|
|
29
|
+
const res = await config.api.configs.saveConfig(data)
|
|
30
|
+
return {
|
|
31
|
+
...data,
|
|
32
|
+
...res.body,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const saveSettingsConfig = async (
|
|
37
|
+
conf?: any,
|
|
38
|
+
_id?: string,
|
|
39
|
+
_rev?: string
|
|
40
|
+
) => {
|
|
41
|
+
const settingsConfig = structures.configs.settings(conf)
|
|
42
|
+
return saveConfig(settingsConfig, _id, _rev)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("POST /api/global/configs", () => {
|
|
38
46
|
describe("google", () => {
|
|
39
47
|
const saveGoogleConfig = async (
|
|
40
48
|
conf?: any,
|
|
@@ -49,20 +57,20 @@ describe("configs", () => {
|
|
|
49
57
|
it("should create activated google config", async () => {
|
|
50
58
|
await saveGoogleConfig()
|
|
51
59
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
|
52
|
-
expect(events.auth.SSOCreated).toBeCalledWith(
|
|
60
|
+
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE)
|
|
53
61
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
|
54
62
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
|
55
|
-
expect(events.auth.SSOActivated).toBeCalledWith(
|
|
56
|
-
await config.deleteConfig(
|
|
63
|
+
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE)
|
|
64
|
+
await config.deleteConfig(ConfigType.GOOGLE)
|
|
57
65
|
})
|
|
58
66
|
|
|
59
67
|
it("should create deactivated google config", async () => {
|
|
60
68
|
await saveGoogleConfig({ activated: false })
|
|
61
69
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
|
62
|
-
expect(events.auth.SSOCreated).toBeCalledWith(
|
|
70
|
+
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE)
|
|
63
71
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
|
64
72
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
|
65
|
-
await config.deleteConfig(
|
|
73
|
+
await config.deleteConfig(ConfigType.GOOGLE)
|
|
66
74
|
})
|
|
67
75
|
})
|
|
68
76
|
|
|
@@ -76,11 +84,11 @@ describe("configs", () => {
|
|
|
76
84
|
googleConf._rev
|
|
77
85
|
)
|
|
78
86
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
|
79
|
-
expect(events.auth.SSOUpdated).toBeCalledWith(
|
|
87
|
+
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE)
|
|
80
88
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
|
81
89
|
expect(events.auth.SSODeactivated).toBeCalledTimes(1)
|
|
82
|
-
expect(events.auth.SSODeactivated).toBeCalledWith(
|
|
83
|
-
await config.deleteConfig(
|
|
90
|
+
expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.GOOGLE)
|
|
91
|
+
await config.deleteConfig(ConfigType.GOOGLE)
|
|
84
92
|
})
|
|
85
93
|
|
|
86
94
|
it("should update google config to activated", async () => {
|
|
@@ -92,11 +100,11 @@ describe("configs", () => {
|
|
|
92
100
|
googleConf._rev
|
|
93
101
|
)
|
|
94
102
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
|
95
|
-
expect(events.auth.SSOUpdated).toBeCalledWith(
|
|
103
|
+
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE)
|
|
96
104
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
|
97
105
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
|
98
|
-
expect(events.auth.SSOActivated).toBeCalledWith(
|
|
99
|
-
await config.deleteConfig(
|
|
106
|
+
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE)
|
|
107
|
+
await config.deleteConfig(ConfigType.GOOGLE)
|
|
100
108
|
})
|
|
101
109
|
})
|
|
102
110
|
})
|
|
@@ -115,20 +123,20 @@ describe("configs", () => {
|
|
|
115
123
|
it("should create activated OIDC config", async () => {
|
|
116
124
|
await saveOIDCConfig()
|
|
117
125
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
|
118
|
-
expect(events.auth.SSOCreated).toBeCalledWith(
|
|
126
|
+
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC)
|
|
119
127
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
|
120
128
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
|
121
|
-
expect(events.auth.SSOActivated).toBeCalledWith(
|
|
122
|
-
await config.deleteConfig(
|
|
129
|
+
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC)
|
|
130
|
+
await config.deleteConfig(ConfigType.OIDC)
|
|
123
131
|
})
|
|
124
132
|
|
|
125
133
|
it("should create deactivated OIDC config", async () => {
|
|
126
134
|
await saveOIDCConfig({ activated: false })
|
|
127
135
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
|
128
|
-
expect(events.auth.SSOCreated).toBeCalledWith(
|
|
136
|
+
expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC)
|
|
129
137
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
|
130
138
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
|
131
|
-
await config.deleteConfig(
|
|
139
|
+
await config.deleteConfig(ConfigType.OIDC)
|
|
132
140
|
})
|
|
133
141
|
})
|
|
134
142
|
|
|
@@ -142,11 +150,11 @@ describe("configs", () => {
|
|
|
142
150
|
oidcConf._rev
|
|
143
151
|
)
|
|
144
152
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
|
145
|
-
expect(events.auth.SSOUpdated).toBeCalledWith(
|
|
153
|
+
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC)
|
|
146
154
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
|
147
155
|
expect(events.auth.SSODeactivated).toBeCalledTimes(1)
|
|
148
|
-
expect(events.auth.SSODeactivated).toBeCalledWith(
|
|
149
|
-
await config.deleteConfig(
|
|
156
|
+
expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.OIDC)
|
|
157
|
+
await config.deleteConfig(ConfigType.OIDC)
|
|
150
158
|
})
|
|
151
159
|
|
|
152
160
|
it("should update OIDC config to activated", async () => {
|
|
@@ -158,11 +166,11 @@ describe("configs", () => {
|
|
|
158
166
|
oidcConf._rev
|
|
159
167
|
)
|
|
160
168
|
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
|
|
161
|
-
expect(events.auth.SSOUpdated).toBeCalledWith(
|
|
169
|
+
expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC)
|
|
162
170
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
|
163
171
|
expect(events.auth.SSOActivated).toBeCalledTimes(1)
|
|
164
|
-
expect(events.auth.SSOActivated).toBeCalledWith(
|
|
165
|
-
await config.deleteConfig(
|
|
172
|
+
expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC)
|
|
173
|
+
await config.deleteConfig(ConfigType.OIDC)
|
|
166
174
|
})
|
|
167
175
|
})
|
|
168
176
|
})
|
|
@@ -179,11 +187,11 @@ describe("configs", () => {
|
|
|
179
187
|
|
|
180
188
|
describe("create", () => {
|
|
181
189
|
it("should create SMTP config", async () => {
|
|
182
|
-
await config.deleteConfig(
|
|
190
|
+
await config.deleteConfig(ConfigType.SMTP)
|
|
183
191
|
await saveSMTPConfig()
|
|
184
192
|
expect(events.email.SMTPUpdated).not.toBeCalled()
|
|
185
193
|
expect(events.email.SMTPCreated).toBeCalledTimes(1)
|
|
186
|
-
await config.deleteConfig(
|
|
194
|
+
await config.deleteConfig(ConfigType.SMTP)
|
|
187
195
|
})
|
|
188
196
|
})
|
|
189
197
|
|
|
@@ -194,24 +202,15 @@ describe("configs", () => {
|
|
|
194
202
|
await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
|
|
195
203
|
expect(events.email.SMTPCreated).not.toBeCalled()
|
|
196
204
|
expect(events.email.SMTPUpdated).toBeCalledTimes(1)
|
|
197
|
-
await config.deleteConfig(
|
|
205
|
+
await config.deleteConfig(ConfigType.SMTP)
|
|
198
206
|
})
|
|
199
207
|
})
|
|
200
208
|
})
|
|
201
209
|
|
|
202
210
|
describe("settings", () => {
|
|
203
|
-
const saveSettingsConfig = async (
|
|
204
|
-
conf?: any,
|
|
205
|
-
_id?: string,
|
|
206
|
-
_rev?: string
|
|
207
|
-
) => {
|
|
208
|
-
const settingsConfig = structures.configs.settings(conf)
|
|
209
|
-
return saveConfig(settingsConfig, _id, _rev)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
211
|
describe("create", () => {
|
|
213
212
|
it("should create settings config with default settings", async () => {
|
|
214
|
-
await config.deleteConfig(
|
|
213
|
+
await config.deleteConfig(ConfigType.SETTINGS)
|
|
215
214
|
|
|
216
215
|
await saveSettingsConfig()
|
|
217
216
|
|
|
@@ -222,7 +221,7 @@ describe("configs", () => {
|
|
|
222
221
|
|
|
223
222
|
it("should create settings config with non-default settings", async () => {
|
|
224
223
|
config.selfHosted()
|
|
225
|
-
await config.deleteConfig(
|
|
224
|
+
await config.deleteConfig(ConfigType.SETTINGS)
|
|
226
225
|
const conf = {
|
|
227
226
|
company: "acme",
|
|
228
227
|
logoUrl: "http://example.com",
|
|
@@ -241,7 +240,7 @@ describe("configs", () => {
|
|
|
241
240
|
describe("update", () => {
|
|
242
241
|
it("should update settings config", async () => {
|
|
243
242
|
config.selfHosted()
|
|
244
|
-
await config.deleteConfig(
|
|
243
|
+
await config.deleteConfig(ConfigType.SETTINGS)
|
|
245
244
|
const settingsConfig = await saveSettingsConfig()
|
|
246
245
|
settingsConfig.config.company = "acme"
|
|
247
246
|
settingsConfig.config.logoUrl = "http://example.com"
|
|
@@ -262,14 +261,43 @@ describe("configs", () => {
|
|
|
262
261
|
})
|
|
263
262
|
})
|
|
264
263
|
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
describe("GET /api/global/configs/checklist", () => {
|
|
265
|
+
it("should return the correct checklist", async () => {
|
|
266
|
+
await config.saveSmtpConfig()
|
|
267
|
+
|
|
268
|
+
const res = await config.api.configs.getConfigChecklist()
|
|
269
|
+
const checklist = res.body
|
|
267
270
|
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
expect(checklist.apps.checked).toBeFalsy()
|
|
272
|
+
expect(checklist.smtp.checked).toBeTruthy()
|
|
273
|
+
expect(checklist.adminUser.checked).toBeTruthy()
|
|
274
|
+
})
|
|
275
|
+
})
|
|
270
276
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
277
|
+
describe("GET /api/global/configs/public", () => {
|
|
278
|
+
it("should return the expected public settings", async () => {
|
|
279
|
+
await saveSettingsConfig()
|
|
280
|
+
|
|
281
|
+
const res = await config.api.configs.getPublicSettings()
|
|
282
|
+
const body = res.body as GetPublicSettingsResponse
|
|
283
|
+
|
|
284
|
+
const expected = {
|
|
285
|
+
_id: "config_settings",
|
|
286
|
+
type: "settings",
|
|
287
|
+
config: {
|
|
288
|
+
company: "Budibase",
|
|
289
|
+
logoUrl: "",
|
|
290
|
+
analyticsEnabled: false,
|
|
291
|
+
google: true,
|
|
292
|
+
googleCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/google/callback`,
|
|
293
|
+
isSSOEnforced: false,
|
|
294
|
+
oidc: false,
|
|
295
|
+
oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`,
|
|
296
|
+
platformUrl: "http://localhost:10000",
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
delete body._rev
|
|
300
|
+
expect(body).toEqual(expected)
|
|
301
|
+
})
|
|
274
302
|
})
|
|
275
303
|
})
|
package/src/environment.ts
CHANGED
|
@@ -26,8 +26,6 @@ function parseIntSafe(number: any) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
|
30
|
-
|
|
31
29
|
const environment = {
|
|
32
30
|
// auth
|
|
33
31
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
|
@@ -51,7 +49,7 @@ const environment = {
|
|
|
51
49
|
CLUSTER_PORT: process.env.CLUSTER_PORT,
|
|
52
50
|
// flags
|
|
53
51
|
NODE_ENV: process.env.NODE_ENV,
|
|
54
|
-
SELF_HOSTED:
|
|
52
|
+
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
|
55
53
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
|
56
54
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
|
57
55
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
|
@@ -71,14 +69,6 @@ const environment = {
|
|
|
71
69
|
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
|
72
70
|
*/
|
|
73
71
|
ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE,
|
|
74
|
-
/**
|
|
75
|
-
* Enable to allow an admin user to login using a password.
|
|
76
|
-
* This can be useful to prevent lockout when configuring SSO.
|
|
77
|
-
* However, this should be turned OFF by default for security purposes.
|
|
78
|
-
*/
|
|
79
|
-
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
|
80
|
-
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
|
81
|
-
: false,
|
|
82
72
|
_set(key: any, value: any) {
|
|
83
73
|
process.env[key] = value
|
|
84
74
|
// @ts-ignore
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,13 @@ import { Event } from "@sentry/types/dist/event"
|
|
|
13
13
|
import Application from "koa"
|
|
14
14
|
import { bootstrap } from "global-agent"
|
|
15
15
|
import * as db from "./db"
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
auth,
|
|
18
|
+
logging,
|
|
19
|
+
events,
|
|
20
|
+
middleware,
|
|
21
|
+
env as coreEnv,
|
|
22
|
+
} from "@budibase/backend-core"
|
|
17
23
|
db.init()
|
|
18
24
|
import Koa from "koa"
|
|
19
25
|
import koaBody from "koa-body"
|
|
@@ -25,7 +31,7 @@ const koaSession = require("koa-session")
|
|
|
25
31
|
const logger = require("koa-pino-logger")
|
|
26
32
|
import destroyable from "server-destroy"
|
|
27
33
|
|
|
28
|
-
if (
|
|
34
|
+
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
|
|
29
35
|
console.warn(
|
|
30
36
|
"Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
|
|
31
37
|
)
|
package/src/sdk/auth/auth.ts
CHANGED
|
@@ -58,8 +58,8 @@ export const reset = async (email: string) => {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// exit if user has sso
|
|
61
|
-
if (await userSdk.
|
|
62
|
-
throw new HTTPError("
|
|
61
|
+
if (await userSdk.isPreventPasswordActions(user)) {
|
|
62
|
+
throw new HTTPError("Password reset is disabled for this user", 400)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// send password reset
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
import { structures } from "../../../tests"
|
|
2
|
-
import * as users from "../users"
|
|
3
|
-
import env from "../../../environment"
|
|
4
2
|
import { mocks } from "@budibase/backend-core/tests"
|
|
3
|
+
import { env } from "@budibase/backend-core"
|
|
4
|
+
import * as users from "../users"
|
|
5
5
|
import { CloudAccount } from "@budibase/types"
|
|
6
|
+
import { isPreventPasswordActions } from "../users"
|
|
7
|
+
|
|
8
|
+
jest.mock("@budibase/pro")
|
|
9
|
+
import * as _pro from "@budibase/pro"
|
|
10
|
+
const pro = jest.mocked(_pro, true)
|
|
6
11
|
|
|
7
12
|
describe("users", () => {
|
|
8
|
-
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe("isPreventPasswordActions", () => {
|
|
18
|
+
it("returns false for non sso user", async () => {
|
|
19
|
+
const user = structures.users.user()
|
|
20
|
+
const result = await users.isPreventPasswordActions(user)
|
|
21
|
+
expect(result).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
9
24
|
it("returns true for sso account user", async () => {
|
|
10
25
|
const user = structures.users.user()
|
|
11
26
|
mocks.accounts.getAccount.mockReturnValue(
|
|
12
27
|
Promise.resolve(structures.accounts.ssoAccount() as CloudAccount)
|
|
13
28
|
)
|
|
14
|
-
const result = await users.
|
|
29
|
+
const result = await users.isPreventPasswordActions(user)
|
|
15
30
|
expect(result).toBe(true)
|
|
16
31
|
})
|
|
17
32
|
|
|
18
33
|
it("returns true for sso user", async () => {
|
|
19
34
|
const user = structures.users.ssoUser()
|
|
20
|
-
const result = await users.
|
|
35
|
+
const result = await users.isPreventPasswordActions(user)
|
|
21
36
|
expect(result).toBe(true)
|
|
22
37
|
})
|
|
23
38
|
|
|
39
|
+
describe("enforced sso", () => {
|
|
40
|
+
it("returns true for all users when sso is enforced", async () => {
|
|
41
|
+
const user = structures.users.user()
|
|
42
|
+
pro.features.isSSOEnforced.mockReturnValue(Promise.resolve(true))
|
|
43
|
+
const result = await users.isPreventPasswordActions(user)
|
|
44
|
+
expect(result).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
24
48
|
describe("sso maintenance mode", () => {
|
|
25
49
|
beforeEach(() => {
|
|
26
50
|
env._set("ENABLE_SSO_MAINTENANCE_MODE", true)
|
|
@@ -33,7 +57,7 @@ describe("users", () => {
|
|
|
33
57
|
describe("non-admin user", () => {
|
|
34
58
|
it("returns true", async () => {
|
|
35
59
|
const user = structures.users.ssoUser()
|
|
36
|
-
const result = await users.
|
|
60
|
+
const result = await users.isPreventPasswordActions(user)
|
|
37
61
|
expect(result).toBe(true)
|
|
38
62
|
})
|
|
39
63
|
})
|
|
@@ -43,7 +67,7 @@ describe("users", () => {
|
|
|
43
67
|
const user = structures.users.ssoUser({
|
|
44
68
|
user: structures.users.adminUser(),
|
|
45
69
|
})
|
|
46
|
-
const result = await users.
|
|
70
|
+
const result = await users.isPreventPasswordActions(user)
|
|
47
71
|
expect(result).toBe(false)
|
|
48
72
|
})
|
|
49
73
|
})
|
package/src/sdk/users/users.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
users as usersCore,
|
|
15
15
|
utils,
|
|
16
16
|
ViewName,
|
|
17
|
+
env as coreEnv,
|
|
17
18
|
} from "@budibase/backend-core"
|
|
18
19
|
import {
|
|
19
20
|
AccountMetadata,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
} from "@budibase/types"
|
|
35
36
|
import { sendEmail } from "../../utilities/email"
|
|
36
37
|
import { EmailTemplatePurpose } from "../../constants"
|
|
37
|
-
import
|
|
38
|
+
import * as pro from "@budibase/pro"
|
|
38
39
|
import * as accountSdk from "../accounts"
|
|
39
40
|
|
|
40
41
|
const PAGE_LIMIT = 8
|
|
@@ -122,8 +123,8 @@ const buildUser = async (
|
|
|
122
123
|
|
|
123
124
|
let hashedPassword
|
|
124
125
|
if (password) {
|
|
125
|
-
if (await
|
|
126
|
-
throw new HTTPError("
|
|
126
|
+
if (await isPreventPasswordActions(user)) {
|
|
127
|
+
throw new HTTPError("Password change is disabled for this user", 400)
|
|
127
128
|
}
|
|
128
129
|
hashedPassword = opts.hashPassword ? await utils.hash(password) : password
|
|
129
130
|
} else if (dbUser) {
|
|
@@ -188,13 +189,18 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
export async function
|
|
192
|
+
export async function isPreventPasswordActions(user: User) {
|
|
192
193
|
// when in maintenance mode we allow sso users with the admin role
|
|
193
194
|
// to perform any password action - this prevents lockout
|
|
194
|
-
if (
|
|
195
|
+
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
|
|
195
196
|
return false
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
// SSO is enforced for all users
|
|
200
|
+
if (await pro.features.isSSOEnforced()) {
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
|
|
198
204
|
// Check local sso
|
|
199
205
|
if (isSSOUser(user)) {
|
|
200
206
|
return true
|
|
@@ -278,7 +284,7 @@ export const save = async (
|
|
|
278
284
|
|
|
279
285
|
if (userGroups.length > 0) {
|
|
280
286
|
for (let groupId of userGroups) {
|
|
281
|
-
groupPromises.push(
|
|
287
|
+
groupPromises.push(pro.groups.addUsers(groupId, [_id]))
|
|
282
288
|
}
|
|
283
289
|
}
|
|
284
290
|
}
|
|
@@ -456,7 +462,7 @@ export const bulkCreate = async (
|
|
|
456
462
|
const groupPromises = []
|
|
457
463
|
const createdUserIds = saved.map(user => user._id)
|
|
458
464
|
for (let groupId of groups) {
|
|
459
|
-
groupPromises.push(
|
|
465
|
+
groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
|
|
460
466
|
}
|
|
461
467
|
await Promise.all(groupPromises)
|
|
462
468
|
}
|
package/src/tests/api/configs.ts
CHANGED
|
@@ -14,6 +14,14 @@ export class ConfigAPI extends TestAPI {
|
|
|
14
14
|
.expect("Content-Type", /json/)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
getPublicSettings = () => {
|
|
18
|
+
return this.request
|
|
19
|
+
.get(`/api/global/configs/public`)
|
|
20
|
+
.set(this.config.defaultHeaders())
|
|
21
|
+
.expect(200)
|
|
22
|
+
.expect("Content-Type", /json/)
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
saveConfig = (data: any) => {
|
|
18
26
|
return this.request
|
|
19
27
|
.post(`/api/global/configs`)
|
package/src/tests/api/email.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { Config } from "../../constants"
|
|
2
1
|
import { utils } from "@budibase/backend-core"
|
|
2
|
+
import {
|
|
3
|
+
SettingsConfig,
|
|
4
|
+
ConfigType,
|
|
5
|
+
SMTPConfig,
|
|
6
|
+
GoogleConfig,
|
|
7
|
+
OIDCConfig,
|
|
8
|
+
} from "@budibase/types"
|
|
3
9
|
|
|
4
|
-
export function oidc(conf?: any) {
|
|
10
|
+
export function oidc(conf?: any): OIDCConfig {
|
|
5
11
|
return {
|
|
6
|
-
type:
|
|
12
|
+
type: ConfigType.OIDC,
|
|
7
13
|
config: {
|
|
8
14
|
configs: [
|
|
9
15
|
{
|
|
@@ -21,9 +27,9 @@ export function oidc(conf?: any) {
|
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export function google(conf?: any) {
|
|
30
|
+
export function google(conf?: any): GoogleConfig {
|
|
25
31
|
return {
|
|
26
|
-
type:
|
|
32
|
+
type: ConfigType.GOOGLE,
|
|
27
33
|
config: {
|
|
28
34
|
clientID: "clientId",
|
|
29
35
|
clientSecret: "clientSecret",
|
|
@@ -33,9 +39,9 @@ export function google(conf?: any) {
|
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
export function smtp(conf?: any) {
|
|
42
|
+
export function smtp(conf?: any): SMTPConfig {
|
|
37
43
|
return {
|
|
38
|
-
type:
|
|
44
|
+
type: ConfigType.SMTP,
|
|
39
45
|
config: {
|
|
40
46
|
port: 12345,
|
|
41
47
|
host: "smtptesthost.com",
|
|
@@ -47,25 +53,26 @@ export function smtp(conf?: any) {
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
export function smtpEthereal() {
|
|
56
|
+
export function smtpEthereal(): SMTPConfig {
|
|
51
57
|
return {
|
|
52
|
-
type:
|
|
58
|
+
type: ConfigType.SMTP,
|
|
53
59
|
config: {
|
|
54
60
|
port: 587,
|
|
55
61
|
host: "smtp.ethereal.email",
|
|
62
|
+
from: "testfrom@test.com",
|
|
56
63
|
secure: false,
|
|
57
64
|
auth: {
|
|
58
|
-
user: "
|
|
59
|
-
pass: "
|
|
65
|
+
user: "wyatt.zulauf29@ethereal.email",
|
|
66
|
+
pass: "tEwDtHBWWxusVWAPfa",
|
|
60
67
|
},
|
|
61
68
|
connectionTimeout: 1000, // must be less than the jest default of 5000
|
|
62
69
|
},
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
export function settings(conf?: any) {
|
|
73
|
+
export function settings(conf?: any): SettingsConfig {
|
|
67
74
|
return {
|
|
68
|
-
type:
|
|
75
|
+
type: ConfigType.SETTINGS,
|
|
69
76
|
config: {
|
|
70
77
|
platformUrl: "http://localhost:10000",
|
|
71
78
|
logoUrl: "",
|
package/src/utilities/email.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import env from "../environment"
|
|
2
|
-
import { EmailTemplatePurpose, TemplateType
|
|
2
|
+
import { EmailTemplatePurpose, TemplateType } from "../constants"
|
|
3
3
|
import { getTemplateByPurpose } from "../constants/templates"
|
|
4
4
|
import { getSettingsTemplateContext } from "./templates"
|
|
5
5
|
import { processString } from "@budibase/string-templates"
|
|
6
6
|
import { getResetPasswordCode, getInviteCode } from "./redis"
|
|
7
|
-
import { User,
|
|
8
|
-
import {
|
|
7
|
+
import { User, SMTPInnerConfig } from "@budibase/types"
|
|
8
|
+
import { configs } from "@budibase/backend-core"
|
|
9
9
|
const nodemailer = require("nodemailer")
|
|
10
10
|
|
|
11
11
|
type SendEmailOpts = {
|
|
@@ -36,24 +36,24 @@ const FULL_EMAIL_PURPOSES = [
|
|
|
36
36
|
EmailTemplatePurpose.CUSTOM,
|
|
37
37
|
]
|
|
38
38
|
|
|
39
|
-
function createSMTPTransport(config
|
|
39
|
+
function createSMTPTransport(config?: SMTPInnerConfig) {
|
|
40
40
|
let options: any
|
|
41
|
-
let secure = config
|
|
41
|
+
let secure = config?.secure
|
|
42
42
|
// default it if not specified
|
|
43
43
|
if (secure == null) {
|
|
44
|
-
secure = config
|
|
44
|
+
secure = config?.port === 465
|
|
45
45
|
}
|
|
46
46
|
if (!TEST_MODE) {
|
|
47
47
|
options = {
|
|
48
|
-
port: config
|
|
49
|
-
host: config
|
|
48
|
+
port: config?.port,
|
|
49
|
+
host: config?.host,
|
|
50
50
|
secure: secure,
|
|
51
|
-
auth: config
|
|
51
|
+
auth: config?.auth,
|
|
52
52
|
}
|
|
53
53
|
options.tls = {
|
|
54
54
|
rejectUnauthorized: false,
|
|
55
55
|
}
|
|
56
|
-
if (config
|
|
56
|
+
if (config?.connectionTimeout) {
|
|
57
57
|
options.connectionTimeout = config.connectionTimeout
|
|
58
58
|
}
|
|
59
59
|
} else {
|
|
@@ -134,57 +134,16 @@ async function buildEmail(
|
|
|
134
134
|
})
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
* Utility function for finding most valid SMTP configuration.
|
|
139
|
-
* @param {object} db The CouchDB database which is to be looked up within.
|
|
140
|
-
* @param {string|null} workspaceId If using finer grain control of configs a workspace can be used.
|
|
141
|
-
* @param {boolean|null} automation Whether or not the configuration is being fetched for an email automation.
|
|
142
|
-
* @return {Promise<object|null>} returns the SMTP configuration if it exists
|
|
143
|
-
*/
|
|
144
|
-
async function getSmtpConfiguration(
|
|
145
|
-
db: Database,
|
|
146
|
-
workspaceId?: string,
|
|
147
|
-
automation?: boolean
|
|
148
|
-
) {
|
|
149
|
-
const params: any = {
|
|
150
|
-
type: Config.SMTP,
|
|
151
|
-
}
|
|
152
|
-
if (workspaceId) {
|
|
153
|
-
params.workspace = workspaceId
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const customConfig = await dbCore.getScopedConfig(db, params)
|
|
157
|
-
|
|
158
|
-
if (customConfig) {
|
|
159
|
-
return customConfig
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Use an SMTP fallback configuration from env variables
|
|
163
|
-
if (!automation && env.SMTP_FALLBACK_ENABLED) {
|
|
164
|
-
return {
|
|
165
|
-
port: env.SMTP_PORT,
|
|
166
|
-
host: env.SMTP_HOST,
|
|
167
|
-
secure: false,
|
|
168
|
-
from: env.SMTP_FROM_ADDRESS,
|
|
169
|
-
auth: {
|
|
170
|
-
user: env.SMTP_USER,
|
|
171
|
-
pass: env.SMTP_PASSWORD,
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
137
|
/**
|
|
178
138
|
* Checks if a SMTP config exists based on passed in parameters.
|
|
179
139
|
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
|
|
180
140
|
*/
|
|
181
|
-
export async function isEmailConfigured(
|
|
141
|
+
export async function isEmailConfigured() {
|
|
182
142
|
// when "testing" or smtp fallback is enabled simply return true
|
|
183
143
|
if (TEST_MODE || env.SMTP_FALLBACK_ENABLED) {
|
|
184
144
|
return true
|
|
185
145
|
}
|
|
186
|
-
const
|
|
187
|
-
const config = await getSmtpConfiguration(db, workspaceId)
|
|
146
|
+
const config = await configs.getSMTPConfig()
|
|
188
147
|
return config != null
|
|
189
148
|
}
|
|
190
149
|
|
|
@@ -202,22 +161,17 @@ export async function sendEmail(
|
|
|
202
161
|
purpose: EmailTemplatePurpose,
|
|
203
162
|
opts: SendEmailOpts
|
|
204
163
|
) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
(await getSmtpConfiguration(db, opts?.workspaceId, opts?.automation)) || {}
|
|
208
|
-
if (Object.keys(config).length === 0 && !TEST_MODE) {
|
|
164
|
+
const config = await configs.getSMTPConfig(opts?.automation)
|
|
165
|
+
if (!config && !TEST_MODE) {
|
|
209
166
|
throw "Unable to find SMTP configuration."
|
|
210
167
|
}
|
|
211
168
|
const transport = createSMTPTransport(config)
|
|
212
169
|
// if there is a link code needed this will retrieve it
|
|
213
170
|
const code = await getLinkCode(purpose, email, opts.user, opts?.info)
|
|
214
|
-
let context
|
|
215
|
-
if (code) {
|
|
216
|
-
context = await getSettingsTemplateContext(purpose, code)
|
|
217
|
-
}
|
|
171
|
+
let context = await getSettingsTemplateContext(purpose, code)
|
|
218
172
|
|
|
219
173
|
let message: any = {
|
|
220
|
-
from: opts?.from || config
|
|
174
|
+
from: opts?.from || config?.from,
|
|
221
175
|
html: await buildEmail(purpose, email, context, {
|
|
222
176
|
user: opts?.user,
|
|
223
177
|
contents: opts?.contents,
|
|
@@ -231,9 +185,9 @@ export async function sendEmail(
|
|
|
231
185
|
bcc: opts?.bcc,
|
|
232
186
|
}
|
|
233
187
|
|
|
234
|
-
if (opts?.subject || config
|
|
188
|
+
if (opts?.subject || config?.subject) {
|
|
235
189
|
message.subject = await processString(
|
|
236
|
-
opts?.subject || config
|
|
190
|
+
(opts?.subject || config?.subject) as string,
|
|
237
191
|
context
|
|
238
192
|
)
|
|
239
193
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tenancy, configs } from "@budibase/backend-core"
|
|
2
2
|
import {
|
|
3
|
-
Config,
|
|
4
3
|
InternalTemplateBinding,
|
|
5
4
|
LOGO_URL,
|
|
6
5
|
EmailTemplatePurpose,
|
|
@@ -10,20 +9,16 @@ const BASE_COMPANY = "Budibase"
|
|
|
10
9
|
|
|
11
10
|
export async function getSettingsTemplateContext(
|
|
12
11
|
purpose: EmailTemplatePurpose,
|
|
13
|
-
code?: string
|
|
12
|
+
code?: string | null
|
|
14
13
|
) {
|
|
15
|
-
|
|
16
|
-
// TODO: use more granular settings in the future if required
|
|
17
|
-
let settings =
|
|
18
|
-
(await dbCore.getScopedConfig(db, { type: Config.SETTINGS })) || {}
|
|
14
|
+
let settings = await configs.getSettingsConfig()
|
|
19
15
|
const URL = settings.platformUrl
|
|
20
16
|
const context: any = {
|
|
21
17
|
[InternalTemplateBinding.LOGO_URL]:
|
|
22
18
|
checkSlashesInUrl(`${URL}/${settings.logoUrl}`) || LOGO_URL,
|
|
23
19
|
[InternalTemplateBinding.PLATFORM_URL]: URL,
|
|
24
20
|
[InternalTemplateBinding.COMPANY]: settings.company || BASE_COMPANY,
|
|
25
|
-
[InternalTemplateBinding.DOCS_URL]:
|
|
26
|
-
settings.docsUrl || "https://docs.budibase.com/",
|
|
21
|
+
[InternalTemplateBinding.DOCS_URL]: "https://docs.budibase.com/",
|
|
27
22
|
[InternalTemplateBinding.LOGIN_URL]: checkSlashesInUrl(
|
|
28
23
|
tenancy.addTenantToUrl(`${URL}/login`)
|
|
29
24
|
),
|