@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.
- package/documentation/access.md +20 -0
- package/documentation/identity.md +3 -0
- package/features/auth.incept.feature +32 -0
- package/features/auth.input.feature +59 -0
- package/features/cache.feature +4 -4
- package/package.json +2 -2
- package/source/HTTP/Context.ts +5 -4
- package/source/directives/auth/Anonymous.ts +3 -3
- package/source/directives/auth/Anyone.ts +3 -3
- package/source/directives/auth/Authorization.ts +14 -10
- package/source/directives/auth/Delegate.ts +2 -3
- package/source/directives/auth/Echo.ts +7 -11
- package/source/directives/auth/Federation.ts +4 -4
- package/source/directives/auth/Id.ts +1 -1
- package/source/directives/auth/Incept.ts +24 -10
- package/source/directives/auth/Input.ts +72 -0
- package/source/directives/auth/Rule.ts +3 -5
- package/source/directives/auth/Scheme.ts +4 -4
- package/source/directives/auth/create.ts +10 -0
- package/source/directives/auth/types.ts +8 -4
- package/source/root.ts +15 -0
- package/transpiled/HTTP/Context.js +4 -4
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/directives/auth/Anonymous.d.ts +2 -2
- package/transpiled/directives/auth/Anonymous.js +2 -2
- package/transpiled/directives/auth/Anonymous.js.map +1 -1
- package/transpiled/directives/auth/Anyone.d.ts +2 -2
- package/transpiled/directives/auth/Anyone.js +2 -2
- package/transpiled/directives/auth/Anyone.js.map +1 -1
- package/transpiled/directives/auth/Authorization.d.ts +3 -3
- package/transpiled/directives/auth/Authorization.js +11 -8
- package/transpiled/directives/auth/Authorization.js.map +1 -1
- package/transpiled/directives/auth/Delegate.d.ts +2 -3
- package/transpiled/directives/auth/Delegate.js.map +1 -1
- package/transpiled/directives/auth/Echo.d.ts +3 -4
- package/transpiled/directives/auth/Echo.js +6 -9
- package/transpiled/directives/auth/Echo.js.map +1 -1
- package/transpiled/directives/auth/Federation.d.ts +2 -2
- package/transpiled/directives/auth/Federation.js.map +1 -1
- package/transpiled/directives/auth/Id.d.ts +1 -1
- package/transpiled/directives/auth/Id.js.map +1 -1
- package/transpiled/directives/auth/Incept.d.ts +4 -3
- package/transpiled/directives/auth/Incept.js +20 -8
- package/transpiled/directives/auth/Incept.js.map +1 -1
- package/transpiled/directives/auth/Input.d.ts +14 -0
- package/transpiled/directives/auth/Input.js +49 -0
- package/transpiled/directives/auth/Input.js.map +1 -0
- package/transpiled/directives/auth/Rule.d.ts +2 -4
- package/transpiled/directives/auth/Rule.js +2 -2
- package/transpiled/directives/auth/Rule.js.map +1 -1
- package/transpiled/directives/auth/Scheme.d.ts +2 -2
- package/transpiled/directives/auth/Scheme.js +3 -3
- package/transpiled/directives/auth/Scheme.js.map +1 -1
- package/transpiled/directives/auth/create.d.ts +2 -0
- package/transpiled/directives/auth/create.js +12 -0
- package/transpiled/directives/auth/create.js.map +1 -0
- package/transpiled/directives/auth/types.d.ts +6 -4
- package/transpiled/root.js +15 -0
- package/transpiled/root.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
package/documentation/access.md
CHANGED
|
@@ -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
|
+
"""
|
package/features/cache.feature
CHANGED
|
@@ -42,7 +42,7 @@ Feature: Caching
|
|
|
42
42
|
"""
|
|
43
43
|
200 OK
|
|
44
44
|
|
|
45
|
-
cache-control:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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": "
|
|
64
|
+
"gitHead": "64010813141330312d1ae7ae4424b172bca3e451"
|
|
65
65
|
}
|
package/source/HTTP/Context.ts
CHANGED
|
@@ -54,11 +54,12 @@ export class Context {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
public async body<T> (): Promise<T> {
|
|
57
|
-
|
|
57
|
+
let value = await read(this)
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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,
|
|
11
|
-
return 'authorization' in
|
|
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
|
|
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,
|
|
11
|
-
return
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
86
|
+
context: Context,
|
|
84
87
|
response: http.OutgoingMessage): Promise<void> {
|
|
85
88
|
await Promise.all(directives.map(async (directive) =>
|
|
86
|
-
directive.settle?.(
|
|
89
|
+
directive.settle?.(context, response)))
|
|
87
90
|
|
|
88
|
-
const 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:
|
|
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:
|
|
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:
|
|
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 {
|
|
1
|
+
import { create } from './create'
|
|
2
2
|
import type { OutgoingMessage } from '../../HTTP'
|
|
3
|
-
import type { Directive, Identity,
|
|
3
|
+
import type { Directive, Identity, Context } from './types'
|
|
4
4
|
|
|
5
5
|
export class Echo implements Directive {
|
|
6
|
-
public authorize (identity: Identity | null,
|
|
7
|
-
if (identity === null && 'authorization' in
|
|
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
|
-
|
|
10
|
+
context.identity ??= create()
|
|
11
11
|
|
|
12
12
|
return true
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
public reply (
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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, _:
|
|
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
|
|
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,
|
|
18
|
-
return identity === null && 'authorization' in
|
|
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 (
|
|
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(
|
|
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:
|
|
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
|
-
|
|
46
|
-
|
|
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 {
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
15
|
-
if (
|
|
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(
|
|
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
|
-
|
|
12
|
+
context: Context,
|
|
11
13
|
parameters: Parameter[]
|
|
12
14
|
) => boolean | Promise<boolean>
|
|
13
15
|
|
|
14
|
-
reply?: (
|
|
16
|
+
reply?: (context: Context) => http.OutgoingMessage | null
|
|
15
17
|
|
|
16
|
-
settle?: (
|
|
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
|
|
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
|