@platformatic/db-authorization 3.29.1 → 3.31.0

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/index.d.ts CHANGED
@@ -47,11 +47,14 @@ export type AuthorizationRule<T> = AuthorizationRuleEntity<T> | AuthorizationRul
47
47
  export type SetupDBAuthorizationUserDecorator = () => Promise<void>
48
48
  export type AddRulesForRoles = <T>(rules: Iterable<AuthorizationRule<T>>) => void
49
49
 
50
+ export type RoleMergeStrategy = 'first-match' | 'most-permissive'
51
+
50
52
  export interface DBAuthorizationPluginOptions<T = any> extends FastifyUserPluginOptions {
51
53
  adminSecret?: string
52
54
  roleKey?: string
53
55
  isRolePath?: boolean
54
56
  anonymousRole?: string
57
+ roleMergeStrategy?: RoleMergeStrategy
55
58
  rules?: Array<AuthorizationRule<T>>
56
59
  }
57
60
 
package/index.js CHANGED
@@ -13,6 +13,7 @@ async function auth (app, opts) {
13
13
  const roleKey = opts.rolePath || opts.roleKey || 'X-PLATFORMATIC-ROLE'
14
14
  const isRolePath = !!opts.rolePath // if `true` the role is intepreted as path like `user.role`
15
15
  const anonymousRole = opts.anonymousRole || 'anonymous'
16
+ const roleMergeStrategy = opts.roleMergeStrategy || 'first-match'
16
17
 
17
18
  app.decorateRequest('setupDBAuthorizationUser', setupUser)
18
19
 
@@ -174,7 +175,7 @@ async function auth (app, opts) {
174
175
  return originalFind({ ...restOpts, where, ctx, fields })
175
176
  }
176
177
  const request = getRequestFromContext(ctx)
177
- const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath)
178
+ const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath, roleMergeStrategy)
178
179
  checkFieldsFromRule(rule.find, fields || Object.keys(app.platformatic.entities[entityKey].fields))
179
180
  where = await fromRuleToWhere(ctx, rule.find, where, request.user)
180
181
 
@@ -186,7 +187,7 @@ async function auth (app, opts) {
186
187
  return originalSave({ ctx, input, fields, ...restOpts })
187
188
  }
188
189
  const request = getRequestFromContext(ctx)
189
- const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath)
190
+ const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath, roleMergeStrategy)
190
191
 
191
192
  if (!rule.save) {
192
193
  throw new Unauthorized()
@@ -237,7 +238,7 @@ async function auth (app, opts) {
237
238
  return originalInsert({ inputs, ctx, fields, ...restOpts })
238
239
  }
239
240
  const request = getRequestFromContext(ctx)
240
- const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath)
241
+ const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath, roleMergeStrategy)
241
242
 
242
243
  if (!rule.save) {
243
244
  throw new Unauthorized()
@@ -268,7 +269,7 @@ async function auth (app, opts) {
268
269
  return originalDelete({ where, ctx, fields, ...restOpts })
269
270
  }
270
271
  const request = getRequestFromContext(ctx)
271
- const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath)
272
+ const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath, roleMergeStrategy)
272
273
 
273
274
  where = await fromRuleToWhere(ctx, rule.delete, where, request.user)
274
275
 
@@ -280,7 +281,7 @@ async function auth (app, opts) {
280
281
  return originalUpdateMany({ ...restOpts, where, ctx, fields })
281
282
  }
282
283
  const request = getRequestFromContext(ctx)
283
- const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath)
284
+ const rule = await findRuleForRequestUser(ctx, rules, roleKey, anonymousRole, isRolePath, roleMergeStrategy)
284
285
 
285
286
  where = await fromRuleToWhere(ctx, rule.updateMany, where, request.user)
286
287
 
@@ -357,11 +358,11 @@ async function fromRuleToWhere (ctx, rule, where, user) {
357
358
  return where
358
359
  }
359
360
 
360
- async function findRuleForRequestUser (ctx, rules, roleKey, anonymousRole, isRolePath = false) {
361
+ async function findRuleForRequestUser (ctx, rules, roleKey, anonymousRole, isRolePath = false, roleMergeStrategy = 'first-match') {
361
362
  const request = getRequestFromContext(ctx)
362
363
  await request.setupDBAuthorizationUser()
363
364
  const roles = getRoles(request, roleKey, anonymousRole, isRolePath)
364
- const rule = findRule(rules, roles)
365
+ const rule = findRule(rules, roles, roleMergeStrategy)
365
366
  if (!rule) {
366
367
  ctx.reply.request.log.warn({ roles, rules }, 'no rule for roles')
367
368
  throw new Unauthorized()
@@ -1,4 +1,3 @@
1
- import { MercuriusContext } from 'mercurius'
2
1
  interface IRules {
3
2
  [key: string]: {
4
3
  role: string,
@@ -11,4 +10,6 @@ interface IRules {
11
10
  }
12
11
  }
13
12
 
14
- export default function findRule (ctx: MercuriusContext, rules: IRules[], roleKey: string, anonymousRole: string)
13
+ export type RoleMergeStrategy = 'first-match' | 'most-permissive'
14
+
15
+ export default function findRule (rules: IRules[], roles: string[], strategy?: RoleMergeStrategy): IRules | null
package/lib/find-rule.js CHANGED
@@ -1,4 +1,13 @@
1
- export function findRule (rules, roles) {
1
+ const PLT_ADMIN_ROLE = 'platformatic-admin'
2
+
3
+ export function findRule (rules, roles, strategy = 'first-match') {
4
+ if (strategy === 'first-match') {
5
+ return findRuleFirstMatch(rules, roles)
6
+ }
7
+ return findRuleMostPermissive(rules, roles)
8
+ }
9
+
10
+ function findRuleFirstMatch (rules, roles) {
2
11
  let found = null
3
12
  for (const rule of rules) {
4
13
  for (const role of roles) {
@@ -13,3 +22,52 @@ export function findRule (rules, roles) {
13
22
  }
14
23
  return found
15
24
  }
25
+
26
+ function findRuleMostPermissive (rules, roles) {
27
+ const matchingRules = []
28
+
29
+ for (const rule of rules) {
30
+ for (const role of roles) {
31
+ if (rule.role === role) {
32
+ matchingRules.push(rule)
33
+ break
34
+ }
35
+ }
36
+ }
37
+
38
+ if (matchingRules.length === 0) {
39
+ return null
40
+ }
41
+
42
+ if (matchingRules.length === 1) {
43
+ return matchingRules[0]
44
+ }
45
+
46
+ const nonAdminRules = matchingRules.filter(rule => rule.role !== PLT_ADMIN_ROLE)
47
+ const rulesToMerge = nonAdminRules.length > 0 ? nonAdminRules : matchingRules
48
+
49
+ if (rulesToMerge.length === 1) {
50
+ return rulesToMerge[0]
51
+ }
52
+
53
+ const mergedRule = { ...rulesToMerge[0] }
54
+
55
+ for (let i = 1; i < rulesToMerge.length; i++) {
56
+ const rule = rulesToMerge[i]
57
+
58
+ for (const key of Object.keys(rule)) {
59
+ if (key === 'role' || key === 'entity') {
60
+ continue
61
+ }
62
+
63
+ const currentValue = mergedRule[key]
64
+ const newValue = rule[key]
65
+
66
+ if (newValue && !currentValue) {
67
+ mergedRule[key] = newValue
68
+ }
69
+ }
70
+ }
71
+
72
+ return mergedRule
73
+ }
package/lib/schema.js CHANGED
@@ -13,6 +13,12 @@ export const AuthSchema = {
13
13
  rolePath: {
14
14
  type: 'string',
15
15
  description: 'The path in the user object that contains the roles.'
16
+ },
17
+ roleMergeStrategy: {
18
+ type: 'string',
19
+ enum: ['first-match', 'most-permissive'],
20
+ default: 'first-match',
21
+ description: 'Strategy for merging permissions when a user has multiple roles. "first-match" returns the first matching rule (default). "most-permissive" merges all matching rules, where truthy permissions win over falsy ones.'
16
22
  }
17
23
  },
18
24
  additionalProperties: true // TODO remove and add proper validation for the rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/db-authorization",
3
- "version": "3.29.1",
3
+ "version": "3.31.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -17,15 +17,15 @@
17
17
  "cleaner-spec-reporter": "^0.5.0",
18
18
  "eslint": "9",
19
19
  "fast-jwt": "^5.0.0",
20
- "fastify": "^5.0.0",
20
+ "fastify": "^5.7.0",
21
21
  "get-jwks": "^11.0.0",
22
22
  "neostandard": "^0.12.0",
23
23
  "tsd": "^0.33.0",
24
24
  "typescript": "^5.5.4",
25
25
  "why-is-node-running": "^2.2.2",
26
26
  "ws": "^8.16.0",
27
- "@platformatic/db-core": "3.29.1",
28
- "@platformatic/sql-mapper": "3.29.1"
27
+ "@platformatic/db-core": "3.31.0",
28
+ "@platformatic/sql-mapper": "3.31.0"
29
29
  },
30
30
  "dependencies": {
31
31
  "@fastify/error": "^4.0.0",