@budibase/worker 2.3.18-alpha.3 → 2.3.18-alpha.30

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.
@@ -30,7 +30,11 @@ describe("/api/global/users", () => {
30
30
  email
31
31
  )
32
32
 
33
- expect(res.body).toEqual({ message: "Invitation has been sent." })
33
+ expect(res.body?.message).toBe("Invitation has been sent.")
34
+ expect(res.body?.unsuccessful.length).toBe(0)
35
+ expect(res.body?.successful.length).toBe(1)
36
+ expect(res.body?.successful[0].email).toBe(email)
37
+
34
38
  expect(sendMailMock).toHaveBeenCalled()
35
39
  expect(code).toBeDefined()
36
40
  expect(events.user.invited).toBeCalledTimes(1)
@@ -38,13 +38,6 @@ function buildInviteMultipleValidation() {
38
38
  ))
39
39
  }
40
40
 
41
- function buildInviteLookupValidation() {
42
- // prettier-ignore
43
- return auth.joiValidator.params(Joi.object({
44
- code: Joi.string().required()
45
- }).unknown(true))
46
- }
47
-
48
41
  const createUserAdminOnly = (ctx: any, next: any) => {
49
42
  if (!ctx.request.body._id) {
50
43
  return auth.adminOnly(ctx, next)
@@ -57,8 +50,8 @@ function buildInviteAcceptValidation() {
57
50
  // prettier-ignore
58
51
  return auth.joiValidator.body(Joi.object({
59
52
  inviteCode: Joi.string().required(),
60
- password: Joi.string().required(),
61
- firstName: Joi.string().required(),
53
+ password: Joi.string().optional(),
54
+ firstName: Joi.string().optional(),
62
55
  lastName: Joi.string().optional(),
63
56
  }).required().unknown(true))
64
57
  }
@@ -88,22 +81,34 @@ router
88
81
  .get("/api/global/roles/:appId")
89
82
  .post(
90
83
  "/api/global/users/invite",
91
- auth.adminOnly,
84
+ auth.builderOrAdmin,
92
85
  buildInviteValidation(),
93
86
  controller.invite
94
87
  )
88
+ .post(
89
+ "/api/global/users/onboard",
90
+ auth.builderOrAdmin,
91
+ buildInviteMultipleValidation(),
92
+ controller.onboardUsers
93
+ )
95
94
  .post(
96
95
  "/api/global/users/multi/invite",
97
- auth.adminOnly,
96
+ auth.builderOrAdmin,
98
97
  buildInviteMultipleValidation(),
99
98
  controller.inviteMultiple
100
99
  )
101
100
 
102
101
  // non-global endpoints
102
+ .get("/api/global/users/invite/:code", controller.checkInvite)
103
+ .post(
104
+ "/api/global/users/invite/update/:code",
105
+ auth.builderOrAdmin,
106
+ controller.updateInvite
107
+ )
103
108
  .get(
104
- "/api/global/users/invite/:code",
105
- buildInviteLookupValidation(),
106
- controller.checkInvite
109
+ "/api/global/users/invites",
110
+ auth.builderOrAdmin,
111
+ controller.getUserInvites
107
112
  )
108
113
  .post(
109
114
  "/api/global/users/invite/accept",
@@ -18,6 +18,8 @@ import accountRoutes from "./system/accounts"
18
18
  import restoreRoutes from "./system/restore"
19
19
 
20
20
  let userGroupRoutes = api.groups
21
+ let auditLogRoutes = api.auditLogs
22
+
21
23
  export const routes: Router[] = [
22
24
  configRoutes,
23
25
  userRoutes,
@@ -32,6 +34,7 @@ export const routes: Router[] = [
32
34
  selfRoutes,
33
35
  licenseRoutes,
34
36
  userGroupRoutes,
37
+ auditLogRoutes,
35
38
  migrationRoutes,
36
39
  accountRoutes,
37
40
  restoreRoutes,
package/src/db/index.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import * as core from "@budibase/backend-core"
2
2
  import env from "../environment"
3
3
 
4
- export const init = () => {
5
- const dbConfig: any = {}
4
+ export function init() {
5
+ const dbConfig: any = {
6
+ replication: true,
7
+ find: true,
8
+ }
9
+
6
10
  if (env.isTest() && !env.COUCH_DB_URL) {
7
11
  dbConfig.inMemory = true
12
+ dbConfig.allDbs = true
8
13
  }
14
+
9
15
  core.init({ db: dbConfig })
10
16
  }
@@ -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: selfHosted,
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,15 @@ 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 { auth, logging, events, middleware } from "@budibase/backend-core"
16
+ import { sdk as proSdk } from "@budibase/pro"
17
+ import {
18
+ auth,
19
+ logging,
20
+ events,
21
+ middleware,
22
+ queue,
23
+ env as coreEnv,
24
+ } from "@budibase/backend-core"
17
25
  db.init()
18
26
  import Koa from "koa"
19
27
  import koaBody from "koa-body"
@@ -23,9 +31,15 @@ import * as redis from "./utilities/redis"
23
31
  const Sentry = require("@sentry/node")
24
32
  const koaSession = require("koa-session")
25
33
  const logger = require("koa-pino-logger")
34
+ const { userAgent } = require("koa-useragent")
35
+
26
36
  import destroyable from "server-destroy"
27
37
 
28
- if (env.ENABLE_SSO_MAINTENANCE_MODE) {
38
+ // configure events to use the pro audit log write
39
+ // can't integrate directly into backend-core due to cyclic issues
40
+ events.processors.init(proSdk.auditLogs.write)
41
+
42
+ if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
29
43
  console.warn(
30
44
  "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
31
45
  )
@@ -43,6 +57,7 @@ app.use(koaBody({ multipart: true }))
43
57
  app.use(koaSession(app))
44
58
  app.use(middleware.logging)
45
59
  app.use(logger(logging.pinoSettings()))
60
+ app.use(userAgent)
46
61
 
47
62
  // authentication
48
63
  app.use(auth.passport.initialize())
@@ -78,6 +93,7 @@ server.on("close", async () => {
78
93
  console.log("Server Closed")
79
94
  await redis.shutdown()
80
95
  await events.shutdown()
96
+ await queue.shutdown()
81
97
  if (!env.isTest()) {
82
98
  process.exit(errCode)
83
99
  }
@@ -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.isPreventSSOPasswords(user)) {
62
- throw new HTTPError("SSO user cannot reset password", 400)
61
+ if (await userSdk.isPreventPasswordActions(user)) {
62
+ return
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
- describe("isPreventSSOPasswords", () => {
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.isPreventSSOPasswords(user)
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.isPreventSSOPasswords(user)
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.isPreventSSOPasswords(user)
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.isPreventSSOPasswords(user)
70
+ const result = await users.isPreventPasswordActions(user)
47
71
  expect(result).toBe(false)
48
72
  })
49
73
  })
@@ -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 { groups as groupsSdk } from "@budibase/pro"
38
+ import * as pro from "@budibase/pro"
38
39
  import * as accountSdk from "../accounts"
39
40
 
40
41
  const PAGE_LIMIT = 8
@@ -56,11 +57,22 @@ export const countUsersByApp = async (appId: string) => {
56
57
  }
57
58
  }
58
59
 
60
+ export const getUsersByAppAccess = async (appId?: string) => {
61
+ const opts: any = {
62
+ include_docs: true,
63
+ limit: 50,
64
+ }
65
+ let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
66
+ appId,
67
+ opts
68
+ )
69
+ return response
70
+ }
71
+
59
72
  export const paginatedUsers = async ({
60
73
  page,
61
74
  email,
62
75
  appId,
63
- userIds,
64
76
  }: SearchUsersRequest = {}) => {
65
77
  const db = tenancy.getGlobalDB()
66
78
  // get one extra document, to have the next page
@@ -122,13 +134,18 @@ const buildUser = async (
122
134
 
123
135
  let hashedPassword
124
136
  if (password) {
125
- if (await isPreventSSOPasswords(user)) {
126
- throw new HTTPError("SSO user cannot set password", 400)
137
+ if (await isPreventPasswordActions(user)) {
138
+ throw new HTTPError("Password change is disabled for this user", 400)
127
139
  }
128
140
  hashedPassword = opts.hashPassword ? await utils.hash(password) : password
129
141
  } else if (dbUser) {
130
142
  hashedPassword = dbUser.password
131
- } else if (opts.requirePassword) {
143
+ }
144
+
145
+ // passwords are never required if sso is enforced
146
+ const requirePasswords =
147
+ opts.requirePassword && !(await pro.features.isSSOEnforced())
148
+ if (!hashedPassword && requirePasswords) {
132
149
  throw "Password must be specified."
133
150
  }
134
151
 
@@ -188,13 +205,18 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
188
205
  }
189
206
  }
190
207
 
191
- export async function isPreventSSOPasswords(user: User) {
208
+ export async function isPreventPasswordActions(user: User) {
192
209
  // when in maintenance mode we allow sso users with the admin role
193
210
  // to perform any password action - this prevents lockout
194
- if (env.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
211
+ if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
195
212
  return false
196
213
  }
197
214
 
215
+ // SSO is enforced for all users
216
+ if (await pro.features.isSSOEnforced()) {
217
+ return true
218
+ }
219
+
198
220
  // Check local sso
199
221
  if (isSSOUser(user)) {
200
222
  return true
@@ -228,7 +250,7 @@ export const save = async (
228
250
  const tenantId = tenancy.getTenantId()
229
251
  const db = tenancy.getGlobalDB()
230
252
 
231
- let { email, _id, userGroups = [] } = user
253
+ let { email, _id, userGroups = [], roles } = user
232
254
 
233
255
  if (!email && !_id) {
234
256
  throw new Error("_id or email is required")
@@ -270,6 +292,10 @@ export const save = async (
270
292
  builtUser.roles = dbUser.roles
271
293
  }
272
294
 
295
+ if (!dbUser && roles?.length) {
296
+ builtUser.roles = { ...roles }
297
+ }
298
+
273
299
  // make sure we set the _id field for a new user
274
300
  // Also if this is a new user, associate groups with them
275
301
  let groupPromises = []
@@ -278,7 +304,7 @@ export const save = async (
278
304
 
279
305
  if (userGroups.length > 0) {
280
306
  for (let groupId of userGroups) {
281
- groupPromises.push(groupsSdk.addUsers(groupId, [_id]))
307
+ groupPromises.push(pro.groups.addUsers(groupId, [_id]))
282
308
  }
283
309
  }
284
310
  }
@@ -456,7 +482,7 @@ export const bulkCreate = async (
456
482
  const groupPromises = []
457
483
  const createdUserIds = saved.map(user => user._id)
458
484
  for (let groupId of groups) {
459
- groupPromises.push(groupsSdk.addUsers(groupId, createdUserIds))
485
+ groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
460
486
  }
461
487
  await Promise.all(groupPromises)
462
488
  }
@@ -631,7 +657,7 @@ export const invite = async (
631
657
  }
632
658
  await sendEmail(user.email, EmailTemplatePurpose.INVITATION, opts)
633
659
  response.successful.push({ email: user.email })
634
- await events.user.invited()
660
+ await events.user.invited(user.email)
635
661
  } catch (e) {
636
662
  console.error(`Failed to send email invitation email=${user.email}`, e)
637
663
  response.unsuccessful.push({
@@ -0,0 +1,26 @@
1
+ import { AuditLogSearchParams, SearchAuditLogsResponse } from "@budibase/types"
2
+ import TestConfiguration from "../TestConfiguration"
3
+ import { TestAPI } from "./base"
4
+
5
+ export class AuditLogAPI extends TestAPI {
6
+ constructor(config: TestConfiguration) {
7
+ super(config)
8
+ }
9
+
10
+ search = async (search: AuditLogSearchParams) => {
11
+ const res = await this.request
12
+ .post("/api/global/auditlogs/search")
13
+ .send(search)
14
+ .set(this.config.defaultHeaders())
15
+ .expect("Content-Type", /json/)
16
+ .expect(200)
17
+ return res.body as SearchAuditLogsResponse
18
+ }
19
+
20
+ download = (search: AuditLogSearchParams) => {
21
+ const query = encodeURIComponent(JSON.stringify(search))
22
+ return this.request
23
+ .get(`/api/global/auditlogs/download?query=${query}`)
24
+ .set(this.config.defaultHeaders())
25
+ }
26
+ }
@@ -61,11 +61,13 @@ export class AuthAPI extends TestAPI {
61
61
 
62
62
  let code: string | undefined
63
63
  if (res.status === 200) {
64
- const emailCall = sendMailMock.mock.calls[0][0]
65
- const parts = emailCall.html.split(
66
- `http://localhost:10000/builder/auth/reset?code=`
67
- )
68
- code = parts[1].split('"')[0].split("&")[0]
64
+ if (sendMailMock.mock.calls.length) {
65
+ const emailCall = sendMailMock.mock.calls[0][0]
66
+ const parts = emailCall.html.split(
67
+ `http://localhost:10000/builder/auth/reset?code=`
68
+ )
69
+ code = parts[1].split('"')[0].split("&")[0]
70
+ }
69
71
  }
70
72
 
71
73
  return { code, res }
@@ -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`)
@@ -13,6 +13,7 @@ export class EmailAPI extends TestAPI {
13
13
  email: "test@test.com",
14
14
  purpose,
15
15
  tenantId: this.config.getTenantId(),
16
+ userId: this.config.user?._id!,
16
17
  })
17
18
  .set(this.config.defaultHeaders())
18
19
  .expect("Content-Type", /json/)
@@ -14,6 +14,7 @@ import { GroupsAPI } from "./groups"
14
14
  import { RolesAPI } from "./roles"
15
15
  import { TemplatesAPI } from "./templates"
16
16
  import { LicenseAPI } from "./license"
17
+ import { AuditLogAPI } from "./auditLogs"
17
18
  export default class API {
18
19
  accounts: AccountAPI
19
20
  auth: AuthAPI
@@ -30,6 +31,7 @@ export default class API {
30
31
  roles: RolesAPI
31
32
  templates: TemplatesAPI
32
33
  license: LicenseAPI
34
+ auditLogs: AuditLogAPI
33
35
 
34
36
  constructor(config: TestConfiguration) {
35
37
  this.accounts = new AccountAPI(config)
@@ -47,5 +49,6 @@ export default class API {
47
49
  this.roles = new RolesAPI(config)
48
50
  this.templates = new TemplatesAPI(config)
49
51
  this.license = new LicenseAPI(config)
52
+ this.auditLogs = new AuditLogAPI(config)
50
53
  }
51
54
  }
@@ -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: Config.OIDC,
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: Config.GOOGLE,
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: Config.SMTP,
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: Config.SMTP,
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: "don.bahringer@ethereal.email",
59
- pass: "yCKSH8rWyUPbnhGYk9",
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: Config.SETTINGS,
75
+ type: ConfigType.SETTINGS,
69
76
  config: {
70
77
  platformUrl: "http://localhost:10000",
71
78
  logoUrl: "",