@budibase/worker 2.3.18-alpha.14 → 2.3.18-alpha.16

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/jest.config.ts CHANGED
@@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
7
7
  preset: "@trendyol/jest-testcontainers",
8
8
  setupFiles: ["./src/tests/jestEnv.ts"],
9
9
  setupFilesAfterEnv: ["./src/tests/jestSetup.ts"],
10
- collectCoverageFrom: ["src/**/*.{js,ts}"],
10
+ collectCoverageFrom: ["src/**/*.{js,ts}", "../backend-core/src/**/*.{js,ts}"],
11
11
  coverageReporters: ["lcov", "json", "clover"],
12
12
  transform: {
13
13
  "^.+\\.ts?$": "@swc/jest",
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.14",
4
+ "version": "2.3.18-alpha.16",
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.14",
40
- "@budibase/pro": "2.3.18-alpha.13",
41
- "@budibase/string-templates": "2.3.18-alpha.14",
42
- "@budibase/types": "2.3.18-alpha.14",
39
+ "@budibase/backend-core": "2.3.18-alpha.16",
40
+ "@budibase/pro": "2.3.18-alpha.15",
41
+ "@budibase/string-templates": "2.3.18-alpha.16",
42
+ "@budibase/types": "2.3.18-alpha.16",
43
43
  "@koa/router": "8.0.8",
44
44
  "@sentry/node": "6.17.7",
45
45
  "@techpass/passport-openidconnect": "0.3.2",
@@ -60,6 +60,7 @@
60
60
  "koa-send": "5.0.1",
61
61
  "koa-session": "5.13.1",
62
62
  "koa-static": "5.0.0",
63
+ "koa-useragent": "^4.1.0",
63
64
  "node-fetch": "2.6.7",
64
65
  "nodemailer": "6.7.2",
65
66
  "passport-google-oauth": "2.0.0",
@@ -100,5 +101,5 @@
100
101
  "typescript": "4.7.3",
101
102
  "update-dotenv": "1.1.1"
102
103
  },
103
- "gitHead": "6303105b71c3ef85e5bc8a89a7fd763cf55179e4"
104
+ "gitHead": "c350c15ae0869875936c804b89fcd46e4da4d8bb"
104
105
  }
@@ -69,8 +69,8 @@ export const login = async (ctx: Ctx<LoginRequest>, next: any) => {
69
69
  "local",
70
70
  async (err: any, user: User, info: any) => {
71
71
  await passportCallback(ctx, user, err, info)
72
- await context.identity.doInUserContext(user, async () => {
73
- await events.auth.login("local")
72
+ await context.identity.doInUserContext(user, ctx, async () => {
73
+ await events.auth.login("local", user.email)
74
74
  })
75
75
  ctx.status = 200
76
76
  }
@@ -207,8 +207,8 @@ export const googleCallback = async (ctx: any, next: any) => {
207
207
  { successRedirect: "/", failureRedirect: "/error" },
208
208
  async (err: any, user: SSOUser, info: any) => {
209
209
  await passportCallback(ctx, user, err, info)
210
- await context.identity.doInUserContext(user, async () => {
211
- await events.auth.login("google-internal")
210
+ await context.identity.doInUserContext(user, ctx, async () => {
211
+ await events.auth.login("google-internal", user.email)
212
212
  })
213
213
  ctx.redirect("/")
214
214
  }
@@ -272,8 +272,8 @@ export const oidcCallback = async (ctx: any, next: any) => {
272
272
  { successRedirect: "/", failureRedirect: "/error" },
273
273
  async (err: any, user: SSOUser, info: any) => {
274
274
  await passportCallback(ctx, user, err, info)
275
- await context.identity.doInUserContext(user, async () => {
276
- await events.auth.login("oidc")
275
+ await context.identity.doInUserContext(user, ctx, async () => {
276
+ await events.auth.login("oidc", user.email)
277
277
  })
278
278
  ctx.redirect("/")
279
279
  }
@@ -17,10 +17,15 @@ import {
17
17
  Ctx,
18
18
  GetPublicOIDCConfigResponse,
19
19
  GetPublicSettingsResponse,
20
+ GoogleInnerConfig,
20
21
  isGoogleConfig,
21
22
  isOIDCConfig,
22
23
  isSettingsConfig,
23
24
  isSMTPConfig,
25
+ OIDCConfigs,
26
+ SettingsInnerConfig,
27
+ SSOConfig,
28
+ SSOConfigType,
24
29
  UserCtx,
25
30
  } from "@budibase/types"
26
31
  import * as pro from "@budibase/pro"
@@ -119,6 +124,61 @@ const getEventFns = async (config: Config, existing?: Config) => {
119
124
  return fns
120
125
  }
121
126
 
127
+ type SSOConfigs = { [key in SSOConfigType]: SSOConfig | undefined }
128
+
129
+ async function getSSOConfigs(): Promise<SSOConfigs> {
130
+ const google = await configs.getGoogleConfig()
131
+ const oidc = await configs.getOIDCConfig()
132
+ return {
133
+ [ConfigType.GOOGLE]: google,
134
+ [ConfigType.OIDC]: oidc,
135
+ }
136
+ }
137
+
138
+ async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
139
+ if (!ssoConfigs) {
140
+ ssoConfigs = await getSSOConfigs()
141
+ }
142
+ return !!Object.values(ssoConfigs).find(c => c?.activated)
143
+ }
144
+
145
+ async function verifySettingsConfig(config: SettingsInnerConfig) {
146
+ if (config.isSSOEnforced) {
147
+ const valid = await hasActivatedConfig()
148
+ if (!valid) {
149
+ throw new Error("Cannot enforce SSO without an activated configuration")
150
+ }
151
+ }
152
+ }
153
+
154
+ async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) {
155
+ const settings = await configs.getSettingsConfig()
156
+ if (settings.isSSOEnforced && !config.activated) {
157
+ // config is being saved as deactivated
158
+ // ensure there is at least one other activated sso config
159
+ const ssoConfigs = await getSSOConfigs()
160
+
161
+ // overwrite the config being updated
162
+ // to reflect the desired state
163
+ ssoConfigs[type] = config
164
+
165
+ const activated = await hasActivatedConfig(ssoConfigs)
166
+ if (!activated) {
167
+ throw new Error(
168
+ "Configuration cannot be deactivated while SSO is enforced"
169
+ )
170
+ }
171
+ }
172
+ }
173
+
174
+ async function verifyGoogleConfig(config: GoogleInnerConfig) {
175
+ await verifySSOConfig(ConfigType.GOOGLE, config)
176
+ }
177
+
178
+ async function verifyOIDCConfig(config: OIDCConfigs) {
179
+ await verifySSOConfig(ConfigType.OIDC, config.configs[0])
180
+ }
181
+
122
182
  export async function save(ctx: UserCtx<Config>) {
123
183
  const body = ctx.request.body
124
184
  const type = body.type
@@ -133,10 +193,19 @@ export async function save(ctx: UserCtx<Config>) {
133
193
 
134
194
  try {
135
195
  // verify the configuration
136
- switch (config.type) {
196
+ switch (type) {
137
197
  case ConfigType.SMTP:
138
198
  await email.verifyConfig(config)
139
199
  break
200
+ case ConfigType.SETTINGS:
201
+ await verifySettingsConfig(config)
202
+ break
203
+ case ConfigType.GOOGLE:
204
+ await verifyGoogleConfig(config)
205
+ break
206
+ case ConfigType.OIDC:
207
+ await verifyOIDCConfig(config)
208
+ break
140
209
  }
141
210
  } catch (err: any) {
142
211
  ctx.throw(400, err)
@@ -34,8 +34,8 @@ function settingValidation() {
34
34
  function googleValidation() {
35
35
  // prettier-ignore
36
36
  return Joi.object({
37
- clientID: Joi.when('activated', { is: true, then: Joi.string().required() }),
38
- clientSecret: Joi.when('activated', { is: true, then: Joi.string().required() }),
37
+ clientID: Joi.string().required(),
38
+ clientSecret: Joi.string().required(),
39
39
  activated: Joi.boolean().required(),
40
40
  }).unknown(true)
41
41
  }
@@ -45,12 +45,12 @@ function oidcValidation() {
45
45
  return Joi.object({
46
46
  configs: Joi.array().items(
47
47
  Joi.object({
48
- clientID: Joi.when('activated', { is: true, then: Joi.string().required() }),
49
- clientSecret: Joi.when('activated', { is: true, then: Joi.string().required() }),
50
- configUrl: Joi.when('activated', { is: true, then: Joi.string().required() }),
48
+ clientID: Joi.string().required(),
49
+ clientSecret: Joi.string().required(),
50
+ configUrl: Joi.string().required(),
51
51
  logo: Joi.string().allow("", null),
52
52
  name: Joi.string().allow("", null),
53
- uuid: Joi.when('activated', { is: true, then: Joi.string().required() }),
53
+ uuid: Joi.string().required(),
54
54
  activated: Joi.boolean().required(),
55
55
  scopes: Joi.array().optional()
56
56
  })
@@ -0,0 +1,111 @@
1
+ import { mocks, structures } from "@budibase/backend-core/tests"
2
+ import { context, events } from "@budibase/backend-core"
3
+ import { Event, IdentityType } from "@budibase/types"
4
+ import { TestConfiguration } from "../../../../tests"
5
+
6
+ mocks.licenses.useAuditLogs()
7
+
8
+ const BASE_IDENTITY = {
9
+ account: undefined,
10
+ type: IdentityType.USER,
11
+ }
12
+ const USER_AUDIT_LOG_COUNT = 3
13
+ const APP_ID = "app_1"
14
+
15
+ describe("/api/global/auditlogs", () => {
16
+ const config = new TestConfiguration()
17
+
18
+ beforeAll(async () => {
19
+ await config.beforeAll()
20
+ })
21
+
22
+ afterAll(async () => {
23
+ await config.afterAll()
24
+ })
25
+
26
+ describe("POST /api/global/auditlogs/search", () => {
27
+ it("should be able to fire some events (create audit logs)", async () => {
28
+ await context.doInTenant(config.tenantId, async () => {
29
+ const userId = config.user!._id!
30
+ const identity = {
31
+ ...BASE_IDENTITY,
32
+ _id: userId,
33
+ tenantId: config.tenantId,
34
+ }
35
+ await context.doInIdentityContext(identity, async () => {
36
+ for (let i = 0; i < USER_AUDIT_LOG_COUNT; i++) {
37
+ await events.user.created(structures.users.user())
38
+ }
39
+ await context.doInAppContext(APP_ID, async () => {
40
+ await events.app.created(structures.apps.app(APP_ID))
41
+ })
42
+ // fetch the user created events
43
+ const response = await config.api.auditLogs.search({
44
+ events: [Event.USER_CREATED],
45
+ })
46
+ expect(response.data).toBeDefined()
47
+ // there will be an initial event which comes from the default user creation
48
+ expect(response.data.length).toBe(USER_AUDIT_LOG_COUNT + 1)
49
+ })
50
+ })
51
+ })
52
+
53
+ it("should be able to search by event", async () => {
54
+ const response = await config.api.auditLogs.search({
55
+ events: [Event.USER_CREATED],
56
+ })
57
+ expect(response.data.length).toBeGreaterThan(0)
58
+ for (let log of response.data) {
59
+ expect(log.event).toBe(Event.USER_CREATED)
60
+ }
61
+ })
62
+
63
+ it("should be able to search by time range (frozen)", async () => {
64
+ // this is frozen, only need to add 1 and minus 1
65
+ const now = new Date()
66
+ const start = new Date()
67
+ start.setSeconds(now.getSeconds() - 1)
68
+ const end = new Date()
69
+ end.setSeconds(now.getSeconds() + 1)
70
+ const response = await config.api.auditLogs.search({
71
+ startDate: start.toISOString(),
72
+ endDate: end.toISOString(),
73
+ })
74
+ expect(response.data.length).toBeGreaterThan(0)
75
+ for (let log of response.data) {
76
+ expect(log.timestamp).toBe(now.toISOString())
77
+ }
78
+ })
79
+
80
+ it("should be able to search by user ID", async () => {
81
+ const userId = config.user!._id!
82
+ const response = await config.api.auditLogs.search({
83
+ userIds: [userId],
84
+ })
85
+ expect(response.data.length).toBeGreaterThan(0)
86
+ for (let log of response.data) {
87
+ expect(log.user._id).toBe(userId)
88
+ }
89
+ })
90
+
91
+ it("should be able to search by app ID", async () => {
92
+ const response = await config.api.auditLogs.search({
93
+ appIds: [APP_ID],
94
+ })
95
+ expect(response.data.length).toBeGreaterThan(0)
96
+ for (let log of response.data) {
97
+ expect(log.app?._id).toBe(APP_ID)
98
+ }
99
+ })
100
+
101
+ it("should be able to search by full string", async () => {
102
+ const response = await config.api.auditLogs.search({
103
+ fullSearch: "User",
104
+ })
105
+ expect(response.data.length).toBeGreaterThan(0)
106
+ for (let log of response.data) {
107
+ expect(log.name.includes("User")).toBe(true)
108
+ }
109
+ })
110
+ })
111
+ })
@@ -367,7 +367,7 @@ describe("/api/global/auth", () => {
367
367
 
368
368
  const res = await config.api.configs.OIDCCallback(configId, preAuthRes)
369
369
 
370
- expect(events.auth.login).toBeCalledWith("oidc")
370
+ expect(events.auth.login).toBeCalledWith("oidc", "oauth@example.com")
371
371
  expect(events.auth.login).toBeCalledTimes(1)
372
372
  expect(res.status).toBe(302)
373
373
  const location: string = res.get("location")
@@ -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
  }
package/src/index.ts CHANGED
@@ -13,11 +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 { sdk as proSdk } from "@budibase/pro"
16
17
  import {
17
18
  auth,
18
19
  logging,
19
20
  events,
20
21
  middleware,
22
+ queue,
21
23
  env as coreEnv,
22
24
  } from "@budibase/backend-core"
23
25
  db.init()
@@ -29,8 +31,14 @@ import * as redis from "./utilities/redis"
29
31
  const Sentry = require("@sentry/node")
30
32
  const koaSession = require("koa-session")
31
33
  const logger = require("koa-pino-logger")
34
+ const { userAgent } = require("koa-useragent")
35
+
32
36
  import destroyable from "server-destroy"
33
37
 
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
+
34
42
  if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
35
43
  console.warn(
36
44
  "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
@@ -49,6 +57,7 @@ app.use(koaBody({ multipart: true }))
49
57
  app.use(koaSession(app))
50
58
  app.use(middleware.logging)
51
59
  app.use(logger(logging.pinoSettings()))
60
+ app.use(userAgent)
52
61
 
53
62
  // authentication
54
63
  app.use(auth.passport.initialize())
@@ -84,6 +93,7 @@ server.on("close", async () => {
84
93
  console.log("Server Closed")
85
94
  await redis.shutdown()
86
95
  await events.shutdown()
96
+ await queue.shutdown()
87
97
  if (!env.isTest()) {
88
98
  process.exit(errCode)
89
99
  }
@@ -637,7 +637,7 @@ export const invite = async (
637
637
  }
638
638
  await sendEmail(user.email, EmailTemplatePurpose.INVITATION, opts)
639
639
  response.successful.push({ email: user.email })
640
- await events.user.invited()
640
+ await events.user.invited(user.email)
641
641
  } catch (e) {
642
642
  console.error(`Failed to send email invitation email=${user.email}`, e)
643
643
  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
+ }
@@ -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
  }
@@ -203,7 +203,7 @@ export async function sendEmail(
203
203
  * @param {object} config an SMTP configuration - this is based on the nodemailer API.
204
204
  * @return {Promise<boolean>} returns true if the configuration is valid.
205
205
  */
206
- export async function verifyConfig(config: any) {
206
+ export async function verifyConfig(config: SMTPInnerConfig) {
207
207
  const transport = createSMTPTransport(config)
208
208
  await transport.verify()
209
209
  }