@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 +2 -2
- package/src/api/controllers/system/tenants.ts +29 -0
- package/src/api/routes/system/tenants.ts +1 -0
- package/src/api/routes/system/tests/tenants.spec.ts +89 -0
- package/src/sdk/tenants/tenants.ts +29 -1
- package/src/sdk/tenants/tests/tenants.spec.ts +162 -0
- package/src/tests/api/tenants.ts +9 -0
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.
|
|
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": "
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
+
})
|
package/src/tests/api/tenants.ts
CHANGED
|
@@ -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
|
}
|