@budibase/worker 3.23.23 → 3.23.24

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "3.23.23",
4
+ "version": "3.23.24",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -109,5 +109,5 @@
109
109
  }
110
110
  }
111
111
  },
112
- "gitHead": "e04af85aab2242716869fa9a8f62cd99f8d5e0da"
112
+ "gitHead": "083c01f79a93ea8819c45370178ded6909dd2ef4"
113
113
  }
@@ -1,4 +1,5 @@
1
1
  import { GetTenantInfoResponse, UserCtx } from "@budibase/types"
2
+ import { LockRequest, LockReason } from "@budibase/types"
2
3
  import * as tenantSdk from "../../../sdk/tenants"
3
4
 
4
5
  export async function destroy(ctx: UserCtx<void, void>) {
@@ -18,6 +19,34 @@ export async function destroy(ctx: UserCtx<void, void>) {
18
19
  }
19
20
  }
20
21
 
22
+ export async function lock(ctx: UserCtx<LockRequest, void>) {
23
+ if (!ctx.internal) {
24
+ ctx.throw(403, "Only internal user can lock a tenant")
25
+ }
26
+
27
+ const { reason } = ctx.request.body
28
+ if (reason !== undefined && !Object.values(LockReason).includes(reason)) {
29
+ ctx.throw(
30
+ 400,
31
+ `Invalid lock reason. Valid values: ${Object.values(LockReason).join(", ")}`
32
+ )
33
+ }
34
+
35
+ const tenantId = ctx.params.tenantId
36
+
37
+ try {
38
+ if (reason) {
39
+ await tenantSdk.lockTenant(tenantId, reason)
40
+ } else {
41
+ await tenantSdk.unlockTenant(tenantId)
42
+ }
43
+ ctx.status = 204
44
+ } catch (err) {
45
+ ctx.log.error(err)
46
+ throw err
47
+ }
48
+ }
49
+
21
50
  export async function info(ctx: UserCtx<void, GetTenantInfoResponse>) {
22
51
  ctx.body = await tenantSdk.tenantInfo(ctx.params.tenantId)
23
52
  }
@@ -2,3 +2,4 @@ import * as controller from "../../controllers/system/tenants"
2
2
  import { adminRoutes } from "../endpointGroups"
3
3
 
4
4
  adminRoutes.delete("/api/system/tenants/:tenantId", controller.destroy)
5
+ adminRoutes.put("/api/system/tenants/:tenantId/lock", controller.lock)
@@ -1,5 +1,13 @@
1
1
  import { TestConfiguration } from "../../../../tests"
2
2
  import { tenancy } from "@budibase/backend-core"
3
+ import { LockReason } from "@budibase/types"
4
+ import * as tenantSdk from "../../../../sdk/tenants"
5
+
6
+ jest.mock("../../../../sdk/tenants", () => ({
7
+ lockTenant: jest.fn(),
8
+ unlockTenant: jest.fn(),
9
+ deleteTenant: jest.fn(),
10
+ }))
3
11
 
4
12
  describe("/api/global/tenants", () => {
5
13
  const config = new TestConfiguration()
@@ -58,4 +66,85 @@ describe("/api/global/tenants", () => {
58
66
  expect(res.body).toEqual(config.adminOnlyResponse())
59
67
  })
60
68
  })
69
+
70
+ describe("PUT /api/system/tenants/:tenantId/lock", () => {
71
+ it("allows locking tenant with valid reason", async () => {
72
+ const user = await config.createTenant()
73
+
74
+ await config.api.tenants.lock(
75
+ user.tenantId,
76
+ {
77
+ reason: LockReason.FREE_TIER,
78
+ },
79
+ {
80
+ headers: config.internalAPIHeaders(),
81
+ }
82
+ )
83
+
84
+ expect(tenantSdk.lockTenant).toHaveBeenCalledWith(
85
+ user.tenantId,
86
+ LockReason.FREE_TIER
87
+ )
88
+ expect(tenantSdk.unlockTenant).not.toHaveBeenCalled()
89
+ })
90
+
91
+ it("unlocks tenant when no reason provided", async () => {
92
+ const user = await config.createTenant()
93
+
94
+ await config.api.tenants.lock(
95
+ user.tenantId,
96
+ {},
97
+ {
98
+ headers: config.internalAPIHeaders(),
99
+ }
100
+ )
101
+
102
+ expect(tenantSdk.unlockTenant).toHaveBeenCalledWith(user.tenantId)
103
+ expect(tenantSdk.lockTenant).not.toHaveBeenCalled()
104
+ })
105
+
106
+ it("rejects invalid lock reason", async () => {
107
+ const user = await config.createTenant()
108
+
109
+ const status = 400
110
+ const res = await config.api.tenants.lock(
111
+ user.tenantId,
112
+ {
113
+ reason: "invalid_reason" as any,
114
+ },
115
+ {
116
+ status,
117
+ headers: config.internalAPIHeaders(),
118
+ }
119
+ )
120
+
121
+ expect(res.body.message).toContain("Invalid lock reason. Valid values:")
122
+ expect(res.body.message).toContain(LockReason.FREE_TIER)
123
+ expect(tenantSdk.lockTenant).not.toHaveBeenCalled()
124
+ expect(tenantSdk.unlockTenant).not.toHaveBeenCalled()
125
+ })
126
+
127
+ it("rejects non-internal user", async () => {
128
+ const user = await config.createTenant()
129
+
130
+ const status = 403
131
+ const res = await config.api.tenants.lock(
132
+ user.tenantId,
133
+ {
134
+ reason: LockReason.FREE_TIER,
135
+ },
136
+ {
137
+ status,
138
+ headers: config.authHeaders(user),
139
+ }
140
+ )
141
+
142
+ expect(res.body).toEqual({
143
+ message: "Only internal user can lock a tenant",
144
+ status,
145
+ })
146
+ expect(tenantSdk.lockTenant).not.toHaveBeenCalled()
147
+ expect(tenantSdk.unlockTenant).not.toHaveBeenCalled()
148
+ })
149
+ })
61
150
  })
@@ -1,5 +1,11 @@
1
- import { db as dbCore, platform, tenancy } from "@budibase/backend-core"
1
+ import {
2
+ configs,
3
+ db as dbCore,
4
+ platform,
5
+ tenancy,
6
+ } from "@budibase/backend-core"
2
7
  import { quotas } from "@budibase/pro"
8
+ import { ConfigType, LockReason, SettingsConfig } from "@budibase/types"
3
9
 
4
10
  export async function deleteTenant(tenantId: string) {
5
11
  await quotas.bustCache()
@@ -8,6 +14,28 @@ export async function deleteTenant(tenantId: string) {
8
14
  await removeGlobalDB(tenantId)
9
15
  }
10
16
 
17
+ export async function lockTenant(tenantId: string, lockReason: LockReason) {
18
+ return await setLock(tenantId, lockReason)
19
+ }
20
+
21
+ export async function unlockTenant(tenantId: string) {
22
+ return await setLock(tenantId)
23
+ }
24
+
25
+ async function setLock(tenantId: string, lockReason?: LockReason) {
26
+ const db = tenancy.getTenantDB(tenantId)
27
+ const settingsConfig = await db.tryGet<SettingsConfig>(
28
+ configs.generateConfigID(ConfigType.SETTINGS)
29
+ )
30
+ if (!settingsConfig?.config) {
31
+ throw new Error(
32
+ `Cannot lock. Settings config not found for tenant ${tenantId}`
33
+ )
34
+ }
35
+ settingsConfig.config.lockedBy = lockReason
36
+ await db.put(settingsConfig)
37
+ }
38
+
11
39
  async function removeGlobalDB(tenantId: string) {
12
40
  try {
13
41
  const db = tenancy.getTenantDB(tenantId)
@@ -0,0 +1,162 @@
1
+ import { structures } from "../../../tests"
2
+ import { lockTenant, unlockTenant } from "../tenants"
3
+ import { configs, tenancy } from "@budibase/backend-core"
4
+ import { LockReason, ConfigType, SettingsConfig } from "@budibase/types"
5
+
6
+ // Mock the backend-core modules
7
+ jest.mock("@budibase/backend-core", () => {
8
+ const actual = jest.requireActual("@budibase/backend-core")
9
+ return {
10
+ ...actual,
11
+ tenancy: {
12
+ ...actual.tenancy,
13
+ getTenantDB: jest.fn(),
14
+ getGlobalDBName: jest.fn(),
15
+ },
16
+ configs: {
17
+ ...actual.configs,
18
+ generateConfigID: jest.fn(),
19
+ },
20
+ db: {
21
+ ...actual.db,
22
+ getAllWorkspaces: jest.fn(),
23
+ getDB: jest.fn(),
24
+ dbExists: jest.fn(),
25
+ getGlobalUserParams: jest.fn(),
26
+ },
27
+ platform: {
28
+ ...actual.platform,
29
+ getPlatformDB: jest.fn(),
30
+ },
31
+ }
32
+ })
33
+
34
+ // Mock the pro module
35
+ jest.mock("@budibase/pro", () => ({
36
+ quotas: {
37
+ bustCache: jest.fn(),
38
+ },
39
+ constants: {
40
+ licenses: {
41
+ CLOUD_FREE_LICENSE: "CLOUD_FREE_LICENSE",
42
+ UNLIMITED_LICENSE: "UNLIMITED_LICENSE",
43
+ },
44
+ },
45
+ licensing: {
46
+ cache: {
47
+ getCachedLicense: jest.fn(),
48
+ },
49
+ },
50
+ }))
51
+
52
+ const mockDb = {
53
+ put: jest.fn(),
54
+ tryGet: jest.fn(),
55
+ }
56
+
57
+ const mockedTenancy = jest.mocked(tenancy)
58
+ const mockedConfigs = jest.mocked(configs)
59
+
60
+ describe("tenants", () => {
61
+ beforeEach(() => {
62
+ jest.clearAllMocks()
63
+ // Setup mock database
64
+ mockedTenancy.getTenantDB.mockReturnValue(mockDb as any)
65
+ // Setup mock config ID generation
66
+ mockedConfigs.generateConfigID.mockReturnValue("config_settings")
67
+ })
68
+
69
+ describe("lockTenant", () => {
70
+ it("should lock tenant with provided reason", async () => {
71
+ const tenantId = structures.tenant.id()
72
+ const lockReason = LockReason.FREE_TIER
73
+
74
+ const settingsConfig: SettingsConfig = {
75
+ _id: "config_settings",
76
+ type: ConfigType.SETTINGS,
77
+ config: {},
78
+ }
79
+
80
+ mockDb.tryGet.mockResolvedValue(settingsConfig)
81
+
82
+ await lockTenant(tenantId, lockReason)
83
+
84
+ expect(mockDb.tryGet).toHaveBeenCalledWith(
85
+ configs.generateConfigID(ConfigType.SETTINGS)
86
+ )
87
+ expect(mockDb.put).toHaveBeenCalledWith({
88
+ ...settingsConfig,
89
+ config: {
90
+ ...settingsConfig.config,
91
+ lockedBy: lockReason,
92
+ },
93
+ })
94
+ })
95
+
96
+ it("should throw error when settings config not found", async () => {
97
+ const tenantId = structures.tenant.id()
98
+ const lockReason = LockReason.FREE_TIER
99
+
100
+ mockDb.tryGet.mockResolvedValue(null)
101
+
102
+ await expect(lockTenant(tenantId, lockReason)).rejects.toThrow(
103
+ `Cannot lock. Settings config not found for tenant ${tenantId}`
104
+ )
105
+ })
106
+
107
+ it("should throw error when settings config has no config property", async () => {
108
+ const tenantId = structures.tenant.id()
109
+ const lockReason = LockReason.FREE_TIER
110
+
111
+ const settingsConfig = {
112
+ _id: "config_settings",
113
+ type: ConfigType.SETTINGS,
114
+ }
115
+
116
+ mockDb.tryGet.mockResolvedValue(settingsConfig)
117
+
118
+ await expect(lockTenant(tenantId, lockReason)).rejects.toThrow(
119
+ `Cannot lock. Settings config not found for tenant ${tenantId}`
120
+ )
121
+ })
122
+ })
123
+
124
+ describe("unlockTenant", () => {
125
+ it("should unlock tenant by removing lock reason", async () => {
126
+ const tenantId = structures.tenant.id()
127
+
128
+ const settingsConfig: SettingsConfig = {
129
+ _id: "config_settings",
130
+ type: ConfigType.SETTINGS,
131
+ config: {
132
+ lockedBy: LockReason.FREE_TIER,
133
+ },
134
+ }
135
+
136
+ mockDb.tryGet.mockResolvedValue(settingsConfig)
137
+
138
+ await unlockTenant(tenantId)
139
+
140
+ expect(mockDb.tryGet).toHaveBeenCalledWith(
141
+ configs.generateConfigID(ConfigType.SETTINGS)
142
+ )
143
+ expect(mockDb.put).toHaveBeenCalledWith({
144
+ ...settingsConfig,
145
+ config: {
146
+ ...settingsConfig.config,
147
+ lockedBy: undefined,
148
+ },
149
+ })
150
+ })
151
+
152
+ it("should throw error when settings config not found", async () => {
153
+ const tenantId = structures.tenant.id()
154
+
155
+ mockDb.tryGet.mockResolvedValue(null)
156
+
157
+ await expect(unlockTenant(tenantId)).rejects.toThrow(
158
+ `Cannot lock. Settings config not found for tenant ${tenantId}`
159
+ )
160
+ })
161
+ })
162
+ })
@@ -1,5 +1,6 @@
1
1
  import TestConfiguration from "../TestConfiguration"
2
2
  import { TestAPI, TestAPIOpts } from "./base"
3
+ import { LockRequest } from "@budibase/types"
3
4
 
4
5
  export class TenantAPI extends TestAPI {
5
6
  config: TestConfiguration
@@ -14,4 +15,12 @@ export class TenantAPI extends TestAPI {
14
15
  .set(opts?.headers)
15
16
  .expect(opts?.status ? opts.status : 204)
16
17
  }
18
+
19
+ lock = (tenantId: string, body: LockRequest, opts?: TestAPIOpts) => {
20
+ return this.request
21
+ .put(`/api/system/tenants/${tenantId}/lock`)
22
+ .send(body)
23
+ .set(opts?.headers)
24
+ .expect(opts?.status ? opts.status : 204)
25
+ }
17
26
  }