@toa.io/extensions.exposition 1.0.0-alpha.107 → 1.0.0-alpha.109

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 (60) hide show
  1. package/documentation/access.md +20 -0
  2. package/documentation/identity.md +3 -0
  3. package/features/auth.incept.feature +32 -0
  4. package/features/auth.input.feature +59 -0
  5. package/features/cache.feature +4 -4
  6. package/package.json +2 -2
  7. package/source/HTTP/Context.ts +5 -4
  8. package/source/directives/auth/Anonymous.ts +3 -3
  9. package/source/directives/auth/Anyone.ts +3 -3
  10. package/source/directives/auth/Authorization.ts +14 -10
  11. package/source/directives/auth/Delegate.ts +2 -3
  12. package/source/directives/auth/Echo.ts +7 -11
  13. package/source/directives/auth/Federation.ts +4 -4
  14. package/source/directives/auth/Id.ts +1 -1
  15. package/source/directives/auth/Incept.ts +24 -10
  16. package/source/directives/auth/Input.ts +72 -0
  17. package/source/directives/auth/Rule.ts +3 -5
  18. package/source/directives/auth/Scheme.ts +4 -4
  19. package/source/directives/auth/create.ts +10 -0
  20. package/source/directives/auth/types.ts +8 -4
  21. package/source/root.ts +15 -0
  22. package/transpiled/HTTP/Context.js +4 -4
  23. package/transpiled/HTTP/Context.js.map +1 -1
  24. package/transpiled/directives/auth/Anonymous.d.ts +2 -2
  25. package/transpiled/directives/auth/Anonymous.js +2 -2
  26. package/transpiled/directives/auth/Anonymous.js.map +1 -1
  27. package/transpiled/directives/auth/Anyone.d.ts +2 -2
  28. package/transpiled/directives/auth/Anyone.js +2 -2
  29. package/transpiled/directives/auth/Anyone.js.map +1 -1
  30. package/transpiled/directives/auth/Authorization.d.ts +3 -3
  31. package/transpiled/directives/auth/Authorization.js +11 -8
  32. package/transpiled/directives/auth/Authorization.js.map +1 -1
  33. package/transpiled/directives/auth/Delegate.d.ts +2 -3
  34. package/transpiled/directives/auth/Delegate.js.map +1 -1
  35. package/transpiled/directives/auth/Echo.d.ts +3 -4
  36. package/transpiled/directives/auth/Echo.js +6 -9
  37. package/transpiled/directives/auth/Echo.js.map +1 -1
  38. package/transpiled/directives/auth/Federation.d.ts +2 -2
  39. package/transpiled/directives/auth/Federation.js.map +1 -1
  40. package/transpiled/directives/auth/Id.d.ts +1 -1
  41. package/transpiled/directives/auth/Id.js.map +1 -1
  42. package/transpiled/directives/auth/Incept.d.ts +4 -3
  43. package/transpiled/directives/auth/Incept.js +20 -8
  44. package/transpiled/directives/auth/Incept.js.map +1 -1
  45. package/transpiled/directives/auth/Input.d.ts +14 -0
  46. package/transpiled/directives/auth/Input.js +49 -0
  47. package/transpiled/directives/auth/Input.js.map +1 -0
  48. package/transpiled/directives/auth/Rule.d.ts +2 -4
  49. package/transpiled/directives/auth/Rule.js +2 -2
  50. package/transpiled/directives/auth/Rule.js.map +1 -1
  51. package/transpiled/directives/auth/Scheme.d.ts +2 -2
  52. package/transpiled/directives/auth/Scheme.js +3 -3
  53. package/transpiled/directives/auth/Scheme.js.map +1 -1
  54. package/transpiled/directives/auth/create.d.ts +2 -0
  55. package/transpiled/directives/auth/create.js +12 -0
  56. package/transpiled/directives/auth/create.js.map +1 -0
  57. package/transpiled/directives/auth/types.d.ts +6 -4
  58. package/transpiled/root.js +15 -0
  59. package/transpiled/root.js.map +1 -1
  60. package/transpiled/tsconfig.tsbuildinfo +1 -1
@@ -133,6 +133,26 @@ directives grant access. The value of the `rule` directive can be a single Rule
133
133
 
134
134
  Access will be granted if an Identity matches a `user-id` placeholder and has a Role of `developer`.
135
135
 
136
+ ### `input`
137
+
138
+ Restricts access based on the request body (which must be an object).
139
+
140
+ ```yaml
141
+ /commits/:id:
142
+ PUT:
143
+ auth:role: [developer, reviewer]
144
+ auth:input:
145
+ - prop: approved
146
+ role: reviewer
147
+ - prop: message
148
+ role: developer
149
+ ```
150
+
151
+ The example above restricts access to the `approved` property of the request body to the identity
152
+ with the `reviewer` role, and the `message` property to the identity with the `developer` role.
153
+
154
+ > `auth:input` directive does not grant access by itself.
155
+
136
156
  ### `delegate`
137
157
 
138
158
  Embeds the value of the current Identity into the request body as a property named after the value
@@ -141,6 +141,9 @@ id: 2428c31ecb6e4a51a24ef52f0c4181b9
141
141
  As a result of processing the above request, the provided Basic credentials associated with the
142
142
  Identity `2428c31ecb6e4a51a24ef52f0c4181b9` are created.
143
143
 
144
+ > `auth:incept` directive may have a `null` value, which means that the Identity will be created
145
+ > without any associated entity.
146
+
144
147
  ## FAQ
145
148
 
146
149
  <dl>
@@ -1,5 +1,37 @@
1
1
  Feature: Identity inception
2
2
 
3
+ Scenario: Non-associated Identity inception
4
+ Given the `identity.basic` database is empty
5
+ When the following request is received:
6
+ """
7
+ POST /identity/ HTTP/1.1
8
+ host: nex.toa.io
9
+ authorization: Basic dXNlcjpwYXNzMTIzNA==
10
+ accept: application/yaml
11
+ """
12
+ Then the following reply is sent:
13
+ """
14
+ 201 Created
15
+ authorization: Token ${{ token }}
16
+
17
+ id: ${{ id }}
18
+ roles: []
19
+ """
20
+ When the following request is received:
21
+ """
22
+ GET /identity/ HTTP/1.1
23
+ host: nex.toa.io
24
+ authorization: Basic dXNlcjpwYXNzMTIzNA==
25
+ accept: application/yaml
26
+ """
27
+ Then the following reply is sent:
28
+ """
29
+ 200 OK
30
+
31
+ id: ${{ id }}
32
+ roles: []
33
+ """
34
+
3
35
  Scenario: Creating new Identity using inception with Basic scheme
4
36
  Given the `users` is running with the following manifest:
5
37
  """yaml
@@ -0,0 +1,59 @@
1
+ Feature: Input properties authorization
2
+
3
+ Background:
4
+ Given the `identity.basic` database contains:
5
+ | _id | authority | username | password |
6
+ | 72cf9b0ab0ac4ab2b8036e4e940ddcae | nex | root | $2b$10$Qq/qnyyU5wjrbDXyWok14OnqAZv/z.pLhz.UddatjI6eHU/rFof4i |
7
+ And the `identity.roles` database contains:
8
+ | _id | identity | role |
9
+ | 9c4702490ff84f2a9e1b1da2ab64bdd4 | 72cf9b0ab0ac4ab2b8036e4e940ddcae | app:b |
10
+
11
+ Scenario: Input properties authorization
12
+ Given the `echo` is running with the following manifest:
13
+ """yaml
14
+ exposition:
15
+ /:
16
+ io:input: [a, b]
17
+ io:output: [a, b]
18
+ anonymous: true
19
+ auth:role: app:b
20
+ auth:input:
21
+ - prop: b
22
+ role: app:b
23
+ PUT: parameters
24
+ """
25
+
26
+ When the following request is received:
27
+ """
28
+ PUT /echo/ HTTP/1.1
29
+ host: nex.toa.io
30
+ accept: application/yaml
31
+ content-type: application/yaml
32
+
33
+ a: foo
34
+ b: bar
35
+ """
36
+ Then the following reply is sent:
37
+ """
38
+ 403 Forbidden
39
+
40
+ Input property is not authorized
41
+ """
42
+ When the following request is received:
43
+ """
44
+ PUT /echo/ HTTP/1.1
45
+ host: nex.toa.io
46
+ authorization: Basic cm9vdDpzZWNyZXQ=
47
+ accept: application/yaml
48
+ content-type: application/yaml
49
+
50
+ a: foo
51
+ b: bar
52
+ """
53
+ Then the following reply is sent:
54
+ """
55
+ 200 OK
56
+
57
+ a: foo
58
+ b: bar
59
+ """
@@ -42,7 +42,7 @@ Feature: Caching
42
42
  """
43
43
  200 OK
44
44
 
45
- cache-control: private
45
+ cache-control: no-store
46
46
  """
47
47
 
48
48
  Scenario: Caching successful response
@@ -99,7 +99,7 @@ Feature: Caching
99
99
  """
100
100
  200 OK
101
101
  authorization: Token ${{ token }}
102
- cache-control: private
102
+ cache-control: no-store
103
103
  """
104
104
  When the following request is received:
105
105
  """
@@ -190,7 +190,7 @@ Feature: Caching
190
190
  """
191
191
  200 OK
192
192
  authorization: Token ${{ token }}
193
- cache-control: private
193
+ cache-control: no-store
194
194
  """
195
195
  When the following request is received:
196
196
  """
@@ -251,7 +251,7 @@ Feature: Caching
251
251
  """
252
252
  200 OK
253
253
  authorization: Token ${{ token }}
254
- cache-control: private
254
+ cache-control: no-store
255
255
  """
256
256
  When the following request is received:
257
257
  """
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.107",
3
+ "version": "1.0.0-alpha.109",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -61,5 +61,5 @@
61
61
  "@types/negotiator": "0.6.1",
62
62
  "jest-esbuild": "0.3.0"
63
63
  },
64
- "gitHead": "0c69afa09e2515ed76f88eaa9c9f5e5c97d8c5db"
64
+ "gitHead": "64010813141330312d1ae7ae4424b172bca3e451"
65
65
  }
@@ -54,11 +54,12 @@ export class Context {
54
54
  }
55
55
 
56
56
  public async body<T> (): Promise<T> {
57
- const value = await read(this)
57
+ let value = await read(this)
58
58
 
59
- return this.pipelines.body.length === 0
60
- ? value
61
- : this.pipelines.body.reduce((value, transform) => transform(value), value)
59
+ for (const transform of this.pipelines.body)
60
+ value = await transform(value)
61
+
62
+ return value
62
63
  }
63
64
 
64
65
  private log (request: IncomingMessage): void {
@@ -1,4 +1,4 @@
1
- import { type Directive, type Input } from './types'
1
+ import { type Directive, type Context } from './types'
2
2
 
3
3
  export class Anonymous implements Directive {
4
4
  private readonly allow: boolean
@@ -7,8 +7,8 @@ export class Anonymous implements Directive {
7
7
  this.allow = allow
8
8
  }
9
9
 
10
- public authorize (_: any, input: Input): boolean {
11
- return 'authorization' in input.request.headers
10
+ public authorize (_: any, context: Context): boolean {
11
+ return 'authorization' in context.request.headers
12
12
  ? false
13
13
  : this.allow
14
14
  }
@@ -1,4 +1,4 @@
1
- import { type Directive, type Input } from './types'
1
+ import { type Directive, type Context } from './types'
2
2
 
3
3
  export class Anyone implements Directive {
4
4
  private readonly allow: boolean
@@ -7,7 +7,7 @@ export class Anyone implements Directive {
7
7
  this.allow = allow
8
8
  }
9
9
 
10
- public authorize (_: any, input: Input): boolean {
11
- return input.identity !== null && this.allow
10
+ public authorize (_: any, context: Context): boolean {
11
+ return context.identity !== null && this.allow
12
12
  }
13
13
  }
@@ -12,9 +12,10 @@ import { Echo } from './Echo'
12
12
  import { Scheme } from './Scheme'
13
13
  import { Delegate } from './Delegate'
14
14
  import { Federation } from './Federation'
15
+ import { Anyone } from './Anyone'
16
+ import { Input } from './Input'
15
17
  import { split } from './split'
16
18
  import { PRIMARY, PROVIDERS } from './schemes'
17
- import { Anyone } from './Anyone'
18
19
  import type { Output } from '../../io'
19
20
  import type { Component } from '@toa.io/core'
20
21
  import type { Remotes } from '../../Remotes'
@@ -26,7 +27,7 @@ import type {
26
27
  Discovery,
27
28
  Extension,
28
29
  Identity,
29
- Input,
30
+ Context,
30
31
  Remote,
31
32
  Schemes
32
33
  } from './types'
@@ -53,22 +54,24 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
53
54
  return match(Class,
54
55
  Role, () => new Role(value as string | string[], this.discovery.roles),
55
56
  Rule, () => new Rule(value as Record<string, string>, this.create.bind(this)),
57
+ Input, () => new Input(value, this.create.bind(this)),
56
58
  Incept, () => new Incept(value as string, this.discovery),
57
59
  Delegate, () => new Delegate(value as string, this.discovery.roles),
58
60
  () => new Class(value))
59
61
  }
60
62
 
61
63
  public async preflight (directives: Directive[],
62
- context: Input,
64
+ context: Context,
63
65
  parameters: Parameter[]): Promise<Output> {
64
66
  context.identity = await this.resolve(context.authority, context.request.headers.authorization)
67
+ directives.sort((a, b) => (a.priority ?? 1) - (b.priority ?? 1))
65
68
 
66
69
  for (const directive of directives) {
67
70
  const allow = await directive.authorize(context.identity, context, parameters)
68
71
 
69
72
  if (allow)
70
73
  if (this.permitted(context))
71
- return directive.reply?.(context.identity) ?? null
74
+ return directive.reply?.(context) ?? null
72
75
  else
73
76
  throw new http.Forbidden()
74
77
  }
@@ -80,12 +83,12 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
80
83
  }
81
84
 
82
85
  public async settle (directives: Directive[],
83
- input: Input,
86
+ context: Context,
84
87
  response: http.OutgoingMessage): Promise<void> {
85
88
  await Promise.all(directives.map(async (directive) =>
86
- directive.settle?.(input, response)))
89
+ directive.settle?.(context, response)))
87
90
 
88
- const identity = input.identity
91
+ const identity = context.identity
89
92
 
90
93
  if (identity === null)
91
94
  return
@@ -98,7 +101,7 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
98
101
  this.tokens ??= await this.discovery.tokens
99
102
 
100
103
  const token = await this.tokens.invoke<string>('encrypt', {
101
- input: { authority: input.authority, identity }
104
+ input: { authority: context.authority, identity }
102
105
  })
103
106
 
104
107
  const authorization = `Token ${token}`
@@ -146,7 +149,7 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
146
149
  return identity
147
150
  }
148
151
 
149
- private permitted (context: Input): boolean {
152
+ private permitted (context: Context): boolean {
150
153
  const permissions = context.identity?.permissions
151
154
 
152
155
  if (permissions === undefined)
@@ -177,7 +180,8 @@ const constructors: Record<string, new (value: any, argument?: any) => Directive
177
180
  scheme: Scheme,
178
181
  echo: Echo,
179
182
  delegate: Delegate,
180
- claims: Federation
183
+ claims: Federation,
184
+ input: Input
181
185
  }
182
186
 
183
187
  const REMOTES: Remote[] = ['basic', 'federation', 'tokens', 'roles', 'bans']
@@ -1,8 +1,7 @@
1
1
  import { BadRequest } from '../../HTTP'
2
- import { type Directive, type Identity } from './types'
3
2
  import { Role } from './Role'
3
+ import type { Context, Directive, Identity } from './types'
4
4
  import type { Component } from '@toa.io/core'
5
- import type { Input } from '../../io'
6
5
 
7
6
  export class Delegate implements Directive {
8
7
  private readonly property: string
@@ -13,7 +12,7 @@ export class Delegate implements Directive {
13
12
  this.discovery = discovery
14
13
  }
15
14
 
16
- public async authorize (identity: Identity | null, context: Input): Promise<boolean> {
15
+ public async authorize (identity: Identity | null, context: Context): Promise<boolean> {
17
16
  if (identity === null)
18
17
  return false
19
18
 
@@ -1,26 +1,22 @@
1
- import { newid } from '@toa.io/generic'
1
+ import { create } from './create'
2
2
  import type { OutgoingMessage } from '../../HTTP'
3
- import type { Directive, Identity, Input } from './types'
3
+ import type { Directive, Identity, Context } from './types'
4
4
 
5
5
  export class Echo implements Directive {
6
- public authorize (identity: Identity | null, input: Input): boolean {
7
- if (identity === null && 'authorization' in input.request.headers)
6
+ public authorize (identity: Identity | null, context: Context): boolean {
7
+ if (identity === null && 'authorization' in context.request.headers)
8
8
  return false
9
9
 
10
- input.identity ??= this.create()
10
+ context.identity ??= create()
11
11
 
12
12
  return true
13
13
  }
14
14
 
15
- public reply (identity: Identity | null): OutgoingMessage {
16
- const body = identity!
15
+ public reply (context: Context): OutgoingMessage {
16
+ const body = context.identity!
17
17
 
18
18
  return body.scheme === null
19
19
  ? { status: 201, body }
20
20
  : { body }
21
21
  }
22
-
23
- private create (): Identity {
24
- return { id: newid(), scheme: null, refresh: false, roles: [] }
25
- }
26
22
  }
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert'
2
- import type { Directive, Identity, Input } from './types'
2
+ import type { Directive, Identity, Context } from './types'
3
3
  import type { Parameter } from '../../RTD'
4
4
 
5
5
  export class Federation implements Directive {
@@ -12,7 +12,7 @@ export class Federation implements Directive {
12
12
  assert.ok(this.matchers.length > 0, '`auth:claims` requires at least one property defined')
13
13
  }
14
14
 
15
- public authorize (identity: Identity | null, context: Input, parameters: Parameter[]): boolean {
15
+ public authorize (identity: Identity | null, context: Context, parameters: Parameter[]): boolean {
16
16
  if (identity === null || !('claims' in identity))
17
17
  return false
18
18
 
@@ -59,7 +59,7 @@ function matches (value: string | string[], reference: string): boolean {
59
59
  : value === reference
60
60
  }
61
61
 
62
- function codomain (iss: string, context: Input): boolean {
62
+ function codomain (iss: string, context: Context): boolean {
63
63
  const hostname = new URL(iss).hostname
64
64
  const dot = hostname.indexOf('.')
65
65
  const basename = dot === -1 ? hostname : hostname.slice(dot)
@@ -67,7 +67,7 @@ function codomain (iss: string, context: Input): boolean {
67
67
  return context.authority.slice(-basename.length) === basename
68
68
  }
69
69
 
70
- type Matcher = (value: string | string[], context: Input, parameters: Parameter[]) => boolean
70
+ type Matcher = (value: string | string[], context: Context, parameters: Parameter[]) => boolean
71
71
 
72
72
  interface Claims {
73
73
  iss: string
@@ -8,7 +8,7 @@ export class Id implements Directive {
8
8
  this.parameter = parameter
9
9
  }
10
10
 
11
- public authorize (identity: Identity | null, _: any, parameters: Parameter[]): boolean {
11
+ public authorize (identity: Identity | null, _: unknown, parameters: Parameter[]): boolean {
12
12
  if (identity === null)
13
13
  return false
14
14
 
@@ -1,31 +1,45 @@
1
+ import assert from 'node:assert'
1
2
  import { type Maybe } from '@toa.io/types'
2
3
  import * as http from '../../HTTP'
3
- import { type Directive, type Discovery, type Identity, type Input, type Schemes } from './types'
4
+ import { type Directive, type Discovery, type Identity, type Context, type Schemes } from './types'
4
5
  import { split } from './split'
6
+ import { create } from './create'
5
7
  import { PROVIDERS } from './schemes'
6
8
 
7
9
  export class Incept implements Directive {
8
- private readonly property: string
10
+ private readonly property: string | null
9
11
  private readonly discovery: Discovery
10
12
  private readonly schemes: Schemes = {} as unknown as Schemes
11
13
 
12
14
  public constructor (property: string, discovery: Discovery) {
15
+ assert.ok(property === null || typeof property === 'string',
16
+ '`auth:incept` value must be a string or null')
17
+
13
18
  this.property = property
14
19
  this.discovery = discovery
15
20
  }
16
21
 
17
- public authorize (identity: Identity | null, input: Input): boolean {
18
- return identity === null && 'authorization' in input.request.headers
22
+ public authorize (identity: Identity | null, context: Context): boolean {
23
+ return identity === null && 'authorization' in context.request.headers
24
+ }
25
+
26
+ public reply (context: Context): http.OutgoingMessage | null {
27
+ if (this.property !== null)
28
+ return null
29
+
30
+ const body = create(context.request.headers.authorization)
31
+
32
+ return { body }
19
33
  }
20
34
 
21
- public async settle (input: Input, response: http.OutgoingMessage): Promise<void> {
22
- const id = response.body?.[this.property]
35
+ public async settle (context: Context, response: http.OutgoingMessage): Promise<void> {
36
+ const id = response.body?.[this.property ?? 'id']
23
37
 
24
38
  if (id === undefined)
25
39
  throw new http.Conflict('Identity inception has failed as the response body ' +
26
40
  `does not contain the '${this.property}' property`)
27
41
 
28
- const [scheme, credentials] = split(input.request.headers.authorization!)
42
+ const [scheme, credentials] = split(context.request.headers.authorization!)
29
43
  const provider = PROVIDERS[scheme]
30
44
 
31
45
  this.schemes[scheme] ??= await this.discovery[provider]
@@ -33,7 +47,7 @@ export class Incept implements Directive {
33
47
  const identity = await this.schemes[scheme]
34
48
  .invoke<Maybe<Identity>>('incept', {
35
49
  input: {
36
- authority: input.authority,
50
+ authority: context.authority,
37
51
  id,
38
52
  credentials
39
53
  }
@@ -42,7 +56,7 @@ export class Incept implements Directive {
42
56
  if (identity instanceof Error)
43
57
  throw new http.UnprocessableEntity(identity)
44
58
 
45
- input.identity = identity
46
- input.identity.scheme = scheme
59
+ context.identity = identity
60
+ context.identity.scheme = scheme
47
61
  }
48
62
  }
@@ -0,0 +1,72 @@
1
+ import { Forbidden } from '../../HTTP'
2
+ import type { Parameter } from '../../RTD'
3
+ import type { Context, Directive, Identity, Create } from './types'
4
+
5
+ export class Input implements Directive {
6
+ public priority = 0
7
+ private readonly statements: Statement[] = []
8
+
9
+ public constructor (declarations: Declaration[], create: Create) {
10
+ this.statements = declarations.map((declaration) => new Statement(declaration, create))
11
+ }
12
+
13
+ public async authorize
14
+ (identity: Identity | null, context: Context, parameters: Parameter[]): Promise<boolean> {
15
+ context.pipelines.body.push(async (body) => this.check(identity, context, parameters, body))
16
+
17
+ return false
18
+ }
19
+
20
+ // eslint-disable-next-line max-params
21
+ private async check (identity: Identity | null, context: Context, parameters: Parameter[], body: unknown): Promise<unknown> {
22
+ if (body === undefined || body === null || body.constructor !== Object)
23
+ return body
24
+
25
+ const settled = await Promise.allSettled(this.statements.map(async (statement) =>
26
+ statement.check(identity, context, parameters, body as Body)))
27
+
28
+ for (const result of settled)
29
+ if (result.status === 'rejected')
30
+ throw result.reason
31
+
32
+ return body
33
+ }
34
+ }
35
+
36
+ class Statement {
37
+ private readonly properties: string[]
38
+ private readonly directives: Directive[] = []
39
+
40
+ public constructor ({ prop, ...directives }: Declaration, create: Create) {
41
+ this.properties = typeof prop === 'string' ? [prop] : prop
42
+
43
+ for (const [name, value] of Object.entries(directives)) {
44
+ const directive = create(name, value)
45
+
46
+ this.directives.push(directive)
47
+ }
48
+ }
49
+
50
+ // eslint-disable-next-line max-params
51
+ public async check (identity: Identity | null, context: Context, parameters: Parameter[], body: Body): Promise<void> {
52
+ const match = this.properties.some((property) => property in body)
53
+
54
+ if (!match)
55
+ return
56
+
57
+ for (const directive of this.directives) {
58
+ const authorized = await directive.authorize(identity, context, parameters)
59
+
60
+ if (!authorized)
61
+ throw new Forbidden('Input property is not authorized')
62
+ }
63
+ }
64
+ }
65
+
66
+ interface Declaration {
67
+ [key: Exclude<string, 'prop'>]: unknown
68
+
69
+ prop: string | string[]
70
+ }
71
+
72
+ type Body = Record<string, unknown>
@@ -1,5 +1,5 @@
1
1
  import { type Parameter } from '../../RTD'
2
- import type { Input, Directive, Identity } from './types'
2
+ import type { Context, Directive, Identity, Create } from './types'
3
3
 
4
4
  export class Rule implements Directive {
5
5
  private readonly directives: Directive[] = []
@@ -13,9 +13,9 @@ export class Rule implements Directive {
13
13
  }
14
14
 
15
15
  public async authorize
16
- (identity: Identity | null, input: Input, parameters: Parameter[]): Promise<boolean> {
16
+ (identity: Identity | null, context: Context, parameters: Parameter[]): Promise<boolean> {
17
17
  for (const directive of this.directives) {
18
- const authorized = await directive.authorize(identity, input, parameters)
18
+ const authorized = await directive.authorize(identity, context, parameters)
19
19
 
20
20
  if (!authorized)
21
21
  return false
@@ -24,5 +24,3 @@ export class Rule implements Directive {
24
24
  return true
25
25
  }
26
26
  }
27
-
28
- type Create = (name: string, value: any, ...args: any[]) => Directive
@@ -1,5 +1,5 @@
1
1
  import * as http from '../../HTTP'
2
- import { type Directive, type Identity, type Input } from './types'
2
+ import { type Directive, type Identity, type Context } from './types'
3
3
  import { split } from './split'
4
4
 
5
5
  export class Scheme implements Directive {
@@ -11,11 +11,11 @@ export class Scheme implements Directive {
11
11
  this.Scheme = scheme[0].toUpperCase() + scheme.substring(1)
12
12
  }
13
13
 
14
- public authorize (_: Identity | null, input: Input): boolean {
15
- if (input.request.headers.authorization === undefined)
14
+ public authorize (_: Identity | null, context: Context): boolean {
15
+ if (context.request.headers.authorization === undefined)
16
16
  return false
17
17
 
18
- const [scheme] = split(input.request.headers.authorization)
18
+ const [scheme] = split(context.request.headers.authorization)
19
19
 
20
20
  if (scheme !== this.scheme)
21
21
  throw new http.Forbidden(this.Scheme +
@@ -0,0 +1,10 @@
1
+ import { newid } from '@toa.io/generic'
2
+ import type { Identity } from './types'
3
+
4
+ export function create (credentials?: string): Identity {
5
+ const scheme = credentials === undefined
6
+ ? null
7
+ : credentials.split(' ')[0]
8
+
9
+ return { id: newid(), scheme, refresh: false, roles: [] }
10
+ }
@@ -5,15 +5,17 @@ import type * as http from '../../HTTP'
5
5
  import type * as io from '../../io'
6
6
 
7
7
  export interface Directive {
8
+ priority?: number
9
+
8
10
  authorize: (
9
11
  identity: Identity | null,
10
- input: Input,
12
+ context: Context,
11
13
  parameters: Parameter[]
12
14
  ) => boolean | Promise<boolean>
13
15
 
14
- reply?: (identity: Identity | null) => http.OutgoingMessage
16
+ reply?: (context: Context) => http.OutgoingMessage | null
15
17
 
16
- settle?: (request: Input, response: http.OutgoingMessage) => Promise<void>
18
+ settle?: (context: Context, response: http.OutgoingMessage) => Promise<void>
17
19
  }
18
20
 
19
21
  export interface Identity {
@@ -32,10 +34,12 @@ export interface Ban {
32
34
  banned: boolean
33
35
  }
34
36
 
35
- export type Input = io.Input & Extension
37
+ export type Context = io.Input & Extension
36
38
  export type AuthenticationResult = Maybe<{ identity: Identity, refresh: boolean }>
37
39
 
38
40
  export type Scheme = 'basic' | 'token' | 'bearer'
39
41
  export type Remote = 'basic' | 'federation' | 'tokens' | 'roles' | 'bans'
40
42
  export type Discovery = Record<Remote, Promise<Component>>
41
43
  export type Schemes = Record<Scheme, Component>
44
+
45
+ export type Create = (name: string, value: any, ...args: any[]) => Directive