@budibase/backend-core 2.33.2 → 2.33.4

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.
Files changed (53) hide show
  1. package/dist/index.js +566 -309
  2. package/dist/index.js.map +4 -4
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +4 -4
  5. package/dist/plugins.js.map +1 -1
  6. package/dist/plugins.js.meta.json +1 -1
  7. package/dist/src/context/identity.js +1 -1
  8. package/dist/src/context/identity.js.map +1 -1
  9. package/dist/src/db/couch/DatabaseImpl.js +1 -1
  10. package/dist/src/db/couch/DatabaseImpl.js.map +1 -1
  11. package/dist/src/environment.js +28 -14
  12. package/dist/src/environment.js.map +1 -1
  13. package/dist/src/events/identification.js +7 -5
  14. package/dist/src/events/identification.js.map +1 -1
  15. package/dist/src/features/features.d.ts +1 -0
  16. package/dist/src/features/features.js +4 -3
  17. package/dist/src/features/features.js.map +1 -1
  18. package/dist/src/index.d.ts +0 -2
  19. package/dist/src/middleware/authenticated.js +16 -8
  20. package/dist/src/middleware/authenticated.js.map +1 -1
  21. package/dist/src/security/roles.d.ts +24 -3
  22. package/dist/src/security/roles.js +210 -51
  23. package/dist/src/security/roles.js.map +1 -1
  24. package/dist/src/sql/sql.js +243 -160
  25. package/dist/src/sql/sql.js.map +1 -1
  26. package/dist/src/sql/sqlTable.js +4 -1
  27. package/dist/src/sql/sqlTable.js.map +1 -1
  28. package/dist/src/tenancy/db.d.ts +0 -3
  29. package/dist/src/tenancy/db.js +0 -31
  30. package/dist/src/tenancy/db.js.map +1 -1
  31. package/dist/src/users/db.d.ts +2 -2
  32. package/dist/src/users/db.js +7 -7
  33. package/dist/src/users/db.js.map +1 -1
  34. package/dist/src/users/users.d.ts +1 -0
  35. package/dist/src/users/users.js +13 -0
  36. package/dist/src/users/users.js.map +1 -1
  37. package/dist/src/users/utils.d.ts +3 -3
  38. package/dist/src/users/utils.js +5 -14
  39. package/dist/src/users/utils.js.map +1 -1
  40. package/package.json +4 -4
  41. package/src/context/identity.ts +1 -1
  42. package/src/db/couch/DatabaseImpl.ts +2 -1
  43. package/src/environment.ts +33 -17
  44. package/src/events/identification.ts +7 -6
  45. package/src/features/features.ts +4 -3
  46. package/src/middleware/authenticated.ts +33 -17
  47. package/src/security/roles.ts +238 -56
  48. package/src/sql/sql.ts +290 -206
  49. package/src/sql/sqlTable.ts +4 -1
  50. package/src/tenancy/db.ts +0 -23
  51. package/src/users/db.ts +12 -9
  52. package/src/users/users.ts +11 -0
  53. package/src/users/utils.ts +12 -18
@@ -1,3 +1,4 @@
1
+ import semver from "semver"
1
2
  import { BuiltinPermissionID, PermissionLevel } from "./permissions"
2
3
  import {
3
4
  prefixRoleID,
@@ -7,9 +8,16 @@ import {
7
8
  doWithDB,
8
9
  } from "../db"
9
10
  import { getAppDB } from "../context"
10
- import { Screen, Role as RoleDoc, RoleUIMetadata } from "@budibase/types"
11
+ import {
12
+ Screen,
13
+ Role as RoleDoc,
14
+ RoleUIMetadata,
15
+ Database,
16
+ App,
17
+ } from "@budibase/types"
11
18
  import cloneDeep from "lodash/fp/cloneDeep"
12
- import { RoleColor } from "@budibase/shared-core"
19
+ import { RoleColor, helpers } from "@budibase/shared-core"
20
+ import { uniqBy } from "lodash"
13
21
 
14
22
  export const BUILTIN_ROLE_IDS = {
15
23
  ADMIN: "ADMIN",
@@ -23,14 +31,6 @@ const BUILTIN_IDS = {
23
31
  BUILDER: "BUILDER",
24
32
  }
25
33
 
26
- // exclude internal roles like builder
27
- const EXTERNAL_BUILTIN_ROLE_IDS = [
28
- BUILTIN_IDS.ADMIN,
29
- BUILTIN_IDS.POWER,
30
- BUILTIN_IDS.BASIC,
31
- BUILTIN_IDS.PUBLIC,
32
- ]
33
-
34
34
  export const RoleIDVersion = {
35
35
  // original version, with a UUID based ID
36
36
  UUID: undefined,
@@ -38,12 +38,20 @@ export const RoleIDVersion = {
38
38
  NAME: "name",
39
39
  }
40
40
 
41
+ function rolesInList(roleIds: string[], ids: string | string[]) {
42
+ if (Array.isArray(ids)) {
43
+ return ids.filter(id => roleIds.includes(id)).length === ids.length
44
+ } else {
45
+ return roleIds.includes(ids)
46
+ }
47
+ }
48
+
41
49
  export class Role implements RoleDoc {
42
50
  _id: string
43
51
  _rev?: string
44
52
  name: string
45
53
  permissionId: string
46
- inherits?: string
54
+ inherits?: string | string[]
47
55
  version?: string
48
56
  permissions: Record<string, PermissionLevel[]> = {}
49
57
  uiMetadata?: RoleUIMetadata
@@ -62,12 +70,70 @@ export class Role implements RoleDoc {
62
70
  this.version = RoleIDVersion.NAME
63
71
  }
64
72
 
65
- addInheritance(inherits: string) {
73
+ addInheritance(inherits?: string | string[]) {
74
+ // make sure IDs are correct format
75
+ if (inherits && typeof inherits === "string") {
76
+ inherits = prefixRoleIDNoBuiltin(inherits)
77
+ } else if (inherits && Array.isArray(inherits)) {
78
+ inherits = inherits.map(prefixRoleIDNoBuiltin)
79
+ }
66
80
  this.inherits = inherits
67
81
  return this
68
82
  }
69
83
  }
70
84
 
85
+ export class RoleHierarchyTraversal {
86
+ allRoles: RoleDoc[]
87
+ opts?: { defaultPublic?: boolean }
88
+
89
+ constructor(allRoles: RoleDoc[], opts?: { defaultPublic?: boolean }) {
90
+ this.allRoles = allRoles
91
+ this.opts = opts
92
+ }
93
+
94
+ walk(role: RoleDoc): RoleDoc[] {
95
+ const opts = this.opts,
96
+ allRoles = this.allRoles
97
+ // this will be a full walked list of roles - which may contain duplicates
98
+ let roleList: RoleDoc[] = []
99
+ if (!role || !role._id) {
100
+ return roleList
101
+ }
102
+ roleList.push(role)
103
+ if (Array.isArray(role.inherits)) {
104
+ for (let roleId of role.inherits) {
105
+ const foundRole = findRole(roleId, allRoles, opts)
106
+ if (foundRole) {
107
+ roleList = roleList.concat(this.walk(foundRole))
108
+ }
109
+ }
110
+ } else {
111
+ const foundRoleIds: string[] = []
112
+ let currentRole: RoleDoc | undefined = role
113
+ while (
114
+ currentRole &&
115
+ currentRole.inherits &&
116
+ !rolesInList(foundRoleIds, currentRole.inherits)
117
+ ) {
118
+ if (Array.isArray(currentRole.inherits)) {
119
+ return roleList.concat(this.walk(currentRole))
120
+ } else {
121
+ foundRoleIds.push(currentRole.inherits)
122
+ currentRole = findRole(currentRole.inherits, allRoles, opts)
123
+ if (currentRole) {
124
+ roleList.push(currentRole)
125
+ }
126
+ }
127
+ // loop now found - stop iterating
128
+ if (helpers.roles.checkForRoleInheritanceLoops(roleList)) {
129
+ break
130
+ }
131
+ }
132
+ }
133
+ return uniqBy(roleList, role => role._id)
134
+ }
135
+ }
136
+
71
137
  const BUILTIN_ROLES = {
72
138
  ADMIN: new Role(
73
139
  BUILTIN_IDS.ADMIN,
@@ -126,7 +192,15 @@ export function getBuiltinRoles(): { [key: string]: RoleDoc } {
126
192
  }
127
193
 
128
194
  export function isBuiltin(role: string) {
129
- return getBuiltinRole(role) !== undefined
195
+ return Object.values(BUILTIN_ROLE_IDS).includes(role)
196
+ }
197
+
198
+ export function prefixRoleIDNoBuiltin(roleId: string) {
199
+ if (isBuiltin(roleId)) {
200
+ return roleId
201
+ } else {
202
+ return prefixRoleID(roleId)
203
+ }
130
204
  }
131
205
 
132
206
  export function getBuiltinRole(roleId: string): Role | undefined {
@@ -154,7 +228,11 @@ export function builtinRoleToNumber(id: string) {
154
228
  if (!role) {
155
229
  break
156
230
  }
157
- role = builtins[role.inherits!]
231
+ if (Array.isArray(role.inherits)) {
232
+ throw new Error("Built-in roles don't support multi-inheritance")
233
+ } else {
234
+ role = builtins[role.inherits!]
235
+ }
158
236
  count++
159
237
  } while (role !== null)
160
238
  return count
@@ -170,12 +248,31 @@ export async function roleToNumber(id: string) {
170
248
  const hierarchy = (await getUserRoleHierarchy(id, {
171
249
  defaultPublic: true,
172
250
  })) as RoleDoc[]
173
- for (let role of hierarchy) {
174
- if (role?.inherits && isBuiltin(role.inherits)) {
251
+ const findNumber = (role: RoleDoc): number => {
252
+ if (!role.inherits) {
253
+ return 0
254
+ }
255
+ if (Array.isArray(role.inherits)) {
256
+ // find the built-in roles, get their number, sort it, then get the last one
257
+ const highestBuiltin: number | undefined = role.inherits
258
+ .map(roleId => {
259
+ const foundRole = hierarchy.find(role => role._id === roleId)
260
+ if (foundRole) {
261
+ return findNumber(foundRole) + 1
262
+ }
263
+ })
264
+ .filter(number => number)
265
+ .sort()
266
+ .pop()
267
+ if (highestBuiltin != undefined) {
268
+ return highestBuiltin
269
+ }
270
+ } else if (isBuiltin(role.inherits)) {
175
271
  return builtinRoleToNumber(role.inherits) + 1
176
272
  }
273
+ return 0
177
274
  }
178
- return 0
275
+ return Math.max(...hierarchy.map(findNumber))
179
276
  }
180
277
 
181
278
  /**
@@ -193,17 +290,31 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
193
290
  : roleId1
194
291
  }
195
292
 
293
+ export function compareRoleIds(roleId1: string, roleId2: string) {
294
+ // make sure both role IDs are prefixed correctly
295
+ return prefixRoleID(roleId1) === prefixRoleID(roleId2)
296
+ }
297
+
298
+ export function externalRole(role: RoleDoc): RoleDoc {
299
+ let _id: string | undefined
300
+ if (role._id) {
301
+ _id = getExternalRoleID(role._id)
302
+ }
303
+ return {
304
+ ...role,
305
+ _id,
306
+ inherits: getExternalRoleIDs(role.inherits, role.version),
307
+ }
308
+ }
309
+
196
310
  /**
197
- * Gets the role object, this is mainly useful for two purposes, to check if the level exists and
198
- * to check if the role inherits any others.
199
- * @param roleId The level ID to lookup.
200
- * @param opts options for the function, like whether to halt errors, instead return public.
201
- * @returns The role object, which may contain an "inherits" property.
311
+ * Given a list of roles, this will pick the role out, accounting for built ins.
202
312
  */
203
- export async function getRole(
313
+ export function findRole(
204
314
  roleId: string,
315
+ roles: RoleDoc[],
205
316
  opts?: { defaultPublic?: boolean }
206
- ): Promise<RoleDoc> {
317
+ ): RoleDoc | undefined {
207
318
  // built in roles mostly come from the in-code implementation,
208
319
  // but can be extended by a doc stored about them (e.g. permissions)
209
320
  let role: RoleDoc | undefined = getBuiltinRole(roleId)
@@ -211,22 +322,53 @@ export async function getRole(
211
322
  // make sure has the prefix (if it has it then it won't be added)
212
323
  roleId = prefixRoleID(roleId)
213
324
  }
214
- try {
215
- const db = getAppDB()
216
- const dbRole = await db.get<RoleDoc>(getDBRoleID(roleId))
217
- role = Object.assign(role || {}, dbRole)
218
- // finalise the ID
219
- role._id = getExternalRoleID(role._id!, role.version)
220
- } catch (err) {
221
- if (!isBuiltin(roleId) && opts?.defaultPublic) {
222
- return cloneDeep(BUILTIN_ROLES.PUBLIC)
223
- }
224
- // only throw an error if there is no role at all
225
- if (!role || Object.keys(role).length === 0) {
226
- throw err
325
+ const dbRole = roles.find(
326
+ role => role._id && compareRoleIds(role._id, roleId)
327
+ )
328
+ if (!dbRole && !isBuiltin(roleId) && opts?.defaultPublic) {
329
+ return cloneDeep(BUILTIN_ROLES.PUBLIC)
330
+ }
331
+ // combine the roles
332
+ role = Object.assign(role || {}, dbRole)
333
+ // finalise the ID
334
+ if (role?._id) {
335
+ role._id = getExternalRoleID(role._id, role.version)
336
+ }
337
+ return Object.keys(role).length === 0 ? undefined : role
338
+ }
339
+
340
+ /**
341
+ * Gets the role object, this is mainly useful for two purposes, to check if the level exists and
342
+ * to check if the role inherits any others.
343
+ * @param roleId The level ID to lookup.
344
+ * @param opts options for the function, like whether to halt errors, instead return public.
345
+ * @returns The role object, which may contain an "inherits" property.
346
+ */
347
+ export async function getRole(
348
+ roleId: string,
349
+ opts?: { defaultPublic?: boolean }
350
+ ): Promise<RoleDoc | undefined> {
351
+ const db = getAppDB()
352
+ const roleList = []
353
+ if (!isBuiltin(roleId)) {
354
+ const role = await db.tryGet<RoleDoc>(getDBRoleID(roleId))
355
+ if (role) {
356
+ roleList.push(role)
227
357
  }
228
358
  }
229
- return role
359
+ return findRole(roleId, roleList, opts)
360
+ }
361
+
362
+ export async function saveRoles(roles: RoleDoc[]) {
363
+ const db = getAppDB()
364
+ await db.bulkDocs(
365
+ roles
366
+ .filter(role => role._id)
367
+ .map(role => ({
368
+ ...role,
369
+ _id: prefixRoleID(role._id!),
370
+ }))
371
+ )
230
372
  }
231
373
 
232
374
  /**
@@ -236,24 +378,18 @@ async function getAllUserRoles(
236
378
  userRoleId: string,
237
379
  opts?: { defaultPublic?: boolean }
238
380
  ): Promise<RoleDoc[]> {
381
+ const allRoles = await getAllRoles()
239
382
  // admins have access to all roles
240
383
  if (userRoleId === BUILTIN_IDS.ADMIN) {
241
- return getAllRoles()
384
+ return allRoles
242
385
  }
243
- let currentRole = await getRole(userRoleId, opts)
244
- let roles = currentRole ? [currentRole] : []
245
- let roleIds = [userRoleId]
386
+
246
387
  // get all the inherited roles
247
- while (
248
- currentRole &&
249
- currentRole.inherits &&
250
- roleIds.indexOf(currentRole.inherits) === -1
251
- ) {
252
- roleIds.push(currentRole.inherits)
253
- currentRole = await getRole(currentRole.inherits)
254
- if (currentRole) {
255
- roles.push(currentRole)
256
- }
388
+ const foundRole = findRole(userRoleId, allRoles, opts)
389
+ let roles: RoleDoc[] = []
390
+ if (foundRole) {
391
+ const traversal = new RoleHierarchyTraversal(allRoles, opts)
392
+ roles = traversal.walk(foundRole)
257
393
  }
258
394
  return roles
259
395
  }
@@ -319,7 +455,7 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
319
455
  }
320
456
  return internal(appDB)
321
457
  }
322
- async function internal(db: any) {
458
+ async function internal(db: Database | undefined) {
323
459
  let roles: RoleDoc[] = []
324
460
  if (db) {
325
461
  const body = await db.allDocs(
@@ -334,8 +470,26 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
334
470
  }
335
471
  const builtinRoles = getBuiltinRoles()
336
472
 
473
+ // exclude internal roles like builder
474
+ let externalBuiltinRoles = []
475
+
476
+ if (!db || (await shouldIncludePowerRole(db))) {
477
+ externalBuiltinRoles = [
478
+ BUILTIN_IDS.ADMIN,
479
+ BUILTIN_IDS.POWER,
480
+ BUILTIN_IDS.BASIC,
481
+ BUILTIN_IDS.PUBLIC,
482
+ ]
483
+ } else {
484
+ externalBuiltinRoles = [
485
+ BUILTIN_IDS.ADMIN,
486
+ BUILTIN_IDS.BASIC,
487
+ BUILTIN_IDS.PUBLIC,
488
+ ]
489
+ }
490
+
337
491
  // need to combine builtin with any DB record of them (for sake of permissions)
338
- for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
492
+ for (let builtinRoleId of externalBuiltinRoles) {
339
493
  const builtinRole = builtinRoles[builtinRoleId]
340
494
  const dbBuiltin = roles.filter(
341
495
  dbRole =>
@@ -366,6 +520,18 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
366
520
  }
367
521
  }
368
522
 
523
+ async function shouldIncludePowerRole(db: Database) {
524
+ const app = await db.tryGet<App>(DocumentType.APP_METADATA)
525
+ const creationVersion = app?.creationVersion
526
+ if (!creationVersion || !semver.valid(creationVersion)) {
527
+ // Old apps don't have creationVersion, so we should include it for backward compatibility
528
+ return true
529
+ }
530
+
531
+ const isGreaterThan3x = semver.gte(creationVersion, "3.0.0")
532
+ return !isGreaterThan3x
533
+ }
534
+
369
535
  export class AccessController {
370
536
  userHierarchies: { [key: string]: string[] }
371
537
  constructor() {
@@ -390,7 +556,10 @@ export class AccessController {
390
556
  this.userHierarchies[userRoleId] = roleIds
391
557
  }
392
558
 
393
- return roleIds?.indexOf(tryingRoleId) !== -1
559
+ return (
560
+ roleIds?.find(roleId => compareRoleIds(roleId, tryingRoleId)) !==
561
+ undefined
562
+ )
394
563
  }
395
564
 
396
565
  async checkScreensAccess(screens: Screen[], userRoleId: string) {
@@ -432,7 +601,7 @@ export function getDBRoleID(roleName: string) {
432
601
  export function getExternalRoleID(roleId: string, version?: string) {
433
602
  // for built-in roles we want to remove the DB role ID element (role_)
434
603
  if (
435
- roleId.startsWith(DocumentType.ROLE) &&
604
+ roleId.startsWith(`${DocumentType.ROLE}${SEPARATOR}`) &&
436
605
  (isBuiltin(roleId) || version === RoleIDVersion.NAME)
437
606
  ) {
438
607
  const parts = roleId.split(SEPARATOR)
@@ -441,3 +610,16 @@ export function getExternalRoleID(roleId: string, version?: string) {
441
610
  }
442
611
  return roleId
443
612
  }
613
+
614
+ export function getExternalRoleIDs(
615
+ roleIds: string | string[] | undefined,
616
+ version?: string
617
+ ) {
618
+ if (!roleIds) {
619
+ return roleIds
620
+ } else if (typeof roleIds === "string") {
621
+ return getExternalRoleID(roleIds, version)
622
+ } else {
623
+ return roleIds.map(roleId => getExternalRoleID(roleId, version))
624
+ }
625
+ }