@toa.io/extensions.exposition 0.24.0-alpha.18 → 0.24.0-alpha.19
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/components/context.toa.yaml +12 -0
- package/components/identity.bans/manifest.toa.yaml +1 -1
- package/components/identity.basic/manifest.toa.yaml +1 -1
- package/components/identity.basic/operations/authenticate.js +1 -2
- package/components/identity.basic/operations/authenticate.js.map +1 -1
- package/components/identity.basic/operations/transit.js.map +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/source/authenticate.ts +0 -1
- package/components/identity.federation/events/principal.js +22 -0
- package/components/identity.federation/manifest.toa.yaml +88 -0
- package/components/identity.federation/operations/assertions-as-values.cjs +45 -0
- package/components/identity.federation/operations/assertions-as-values.cjs.map +1 -0
- package/components/identity.federation/operations/assertions-as-values.d.cts +4 -0
- package/components/identity.federation/operations/authenticate.d.ts +3 -0
- package/components/identity.federation/operations/authenticate.js +20 -0
- package/components/identity.federation/operations/authenticate.js.map +1 -0
- package/components/identity.federation/operations/create.d.ts +10 -0
- package/components/identity.federation/operations/create.js +15 -0
- package/components/identity.federation/operations/create.js.map +1 -0
- package/components/identity.federation/operations/jwt.cjs +112 -0
- package/components/identity.federation/operations/jwt.cjs.map +1 -0
- package/components/identity.federation/operations/jwt.d.cts +19 -0
- package/components/identity.federation/operations/schemas.d.ts +43 -0
- package/components/identity.federation/operations/schemas.js +9 -0
- package/components/identity.federation/operations/schemas.js.map +1 -0
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -0
- package/components/identity.federation/operations/types.d.ts +51 -0
- package/components/identity.federation/operations/types.js +3 -0
- package/components/identity.federation/operations/types.js.map +1 -0
- package/components/identity.federation/source/assertions-as-values.cts +20 -0
- package/components/identity.federation/source/authenticate.ts +28 -0
- package/components/identity.federation/source/create.ts +26 -0
- package/components/identity.federation/source/jwt.cts +143 -0
- package/components/identity.federation/source/schemas.ts +45 -0
- package/components/identity.federation/source/types.ts +56 -0
- package/components/identity.federation/tsconfig.json +9 -0
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/manifest.toa.yaml +1 -1
- package/components/identity.tokens/operations/authenticate.js.map +1 -1
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/cucumber.js +0 -1
- package/documentation/components.md +24 -1
- package/documentation/identity.md +7 -7
- package/documentation/protocol.md +20 -1
- package/documentation/query.md +1 -1
- package/documentation/vary.md +69 -0
- package/features/cors.feature +6 -26
- package/features/identity.feature +17 -1
- package/features/identity.federation.feature +125 -0
- package/features/response.feature +0 -0
- package/features/steps/Captures.ts +5 -0
- package/features/steps/Components.ts +5 -0
- package/features/steps/HTTP.ts +33 -5
- package/features/steps/IdP.ts +120 -0
- package/features/steps/Parameters.ts +8 -2
- package/features/steps/Workspace.ts +5 -7
- package/features/vary.feature +120 -0
- package/package.json +17 -18
- package/source/Directive.test.ts +8 -2
- package/source/Directive.ts +19 -16
- package/source/Factory.ts +8 -7
- package/source/Gateway.ts +22 -8
- package/source/HTTP/Server.fixtures.ts +0 -1
- package/source/HTTP/Server.test.ts +61 -138
- package/source/HTTP/Server.ts +45 -31
- package/source/HTTP/formats/text.ts +1 -1
- package/source/HTTP/formats/yaml.ts +1 -1
- package/source/HTTP/messages.ts +8 -2
- package/source/Interception.ts +24 -0
- package/source/RTD/Directives.ts +2 -2
- package/source/RTD/syntax/parse.ts +6 -6
- package/source/RTD/syntax/types.ts +1 -1
- package/source/directives/auth/{Family.ts → Authorization.ts} +29 -33
- package/source/directives/auth/Incept.ts +1 -1
- package/source/directives/auth/Rule.ts +2 -2
- package/source/directives/auth/index.ts +2 -2
- package/source/directives/auth/schemes.ts +2 -1
- package/source/directives/auth/types.ts +9 -6
- package/source/directives/cache/{Family.ts → Cache.ts} +4 -5
- package/source/directives/cache/index.ts +2 -2
- package/source/directives/cache/types.ts +1 -1
- package/source/directives/cors/CORS.ts +52 -0
- package/source/directives/cors/index.ts +3 -0
- package/source/directives/dev/{Family.ts → Development.ts} +3 -4
- package/source/directives/dev/Stub.ts +4 -4
- package/source/directives/dev/Throw.ts +4 -4
- package/source/directives/dev/index.ts +2 -2
- package/source/directives/dev/types.ts +1 -1
- package/source/directives/index.ts +10 -6
- package/source/directives/octets/Context.ts +1 -1
- package/source/directives/octets/Delete.ts +1 -2
- package/source/directives/octets/Fetch.ts +1 -1
- package/source/directives/octets/List.ts +1 -1
- package/source/directives/octets/{Family.ts → Octets.ts} +3 -4
- package/source/directives/octets/Permute.ts +1 -1
- package/source/directives/octets/Store.ts +3 -3
- package/source/directives/octets/index.ts +2 -2
- package/source/directives/octets/types.ts +3 -3
- package/source/directives/vary/Directive.ts +6 -0
- package/source/directives/vary/Embed.ts +62 -0
- package/source/directives/vary/Properties.ts +17 -0
- package/source/directives/vary/Vary.ts +48 -0
- package/source/directives/vary/embeddings/Embedding.ts +6 -0
- package/source/directives/vary/embeddings/Header.ts +30 -0
- package/source/directives/vary/embeddings/Language.ts +31 -0
- package/source/directives/vary/embeddings/index.ts +11 -0
- package/source/directives/vary/index.ts +3 -0
- package/source/io.ts +4 -0
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Directive.d.ts +6 -7
- package/transpiled/Directive.js +12 -10
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Factory.d.ts +0 -1
- package/transpiled/Factory.js +7 -6
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +8 -5
- package/transpiled/Gateway.js +12 -2
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Server.d.ts +4 -2
- package/transpiled/HTTP/Server.fixtures.d.ts +0 -1
- package/transpiled/HTTP/Server.fixtures.js +1 -2
- package/transpiled/HTTP/Server.fixtures.js.map +1 -1
- package/transpiled/HTTP/Server.js +33 -22
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/formats/text.d.ts +3 -1
- package/transpiled/HTTP/formats/text.js.map +1 -1
- package/transpiled/HTTP/formats/yaml.js +1 -1
- package/transpiled/HTTP/formats/yaml.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +4 -0
- package/transpiled/HTTP/messages.js +4 -2
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/Interception.d.ts +9 -0
- package/transpiled/Interception.js +19 -0
- package/transpiled/Interception.js.map +1 -0
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +2 -2
- package/transpiled/RTD/Node.js.map +1 -1
- package/transpiled/RTD/Route.js.map +1 -1
- package/transpiled/RTD/syntax/parse.js +1 -1
- package/transpiled/RTD/syntax/parse.js.map +1 -1
- package/transpiled/RTD/syntax/types.js +1 -1
- package/transpiled/RTD/syntax/types.js.map +1 -1
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/auth/{Family.d.ts → Authorization.d.ts} +4 -4
- package/transpiled/directives/auth/{Family.js → Authorization.js} +15 -8
- package/transpiled/directives/auth/Authorization.js.map +1 -0
- package/transpiled/directives/auth/Incept.js.map +1 -1
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/auth/Rule.d.ts +2 -2
- package/transpiled/directives/auth/Rule.js.map +1 -1
- package/transpiled/directives/auth/index.d.ts +2 -2
- package/transpiled/directives/auth/index.js +4 -5
- package/transpiled/directives/auth/index.js.map +1 -1
- package/transpiled/directives/auth/schemes.js +2 -1
- package/transpiled/directives/auth/schemes.js.map +1 -1
- package/transpiled/directives/auth/types.d.ts +4 -4
- package/transpiled/directives/cache/{Family.d.ts → Cache.d.ts} +4 -5
- package/transpiled/directives/cache/{Family.js → Cache.js} +4 -2
- package/transpiled/directives/cache/{Family.js.map → Cache.js.map} +1 -1
- package/transpiled/directives/cache/index.d.ts +2 -2
- package/transpiled/directives/cache/index.js +4 -5
- package/transpiled/directives/cache/index.js.map +1 -1
- package/transpiled/directives/cache/types.d.ts +1 -1
- package/transpiled/directives/cors/CORS.d.ts +14 -0
- package/transpiled/directives/cors/CORS.js +42 -0
- package/transpiled/directives/cors/CORS.js.map +1 -0
- package/transpiled/directives/cors/index.d.ts +2 -0
- package/transpiled/directives/cors/index.js +6 -0
- package/transpiled/directives/cors/index.js.map +1 -0
- package/transpiled/directives/dev/{Family.d.ts → Development.d.ts} +3 -4
- package/transpiled/directives/dev/{Family.js → Development.js} +4 -2
- package/transpiled/directives/dev/Development.js.map +1 -0
- package/transpiled/directives/dev/Stub.d.ts +3 -3
- package/transpiled/directives/dev/Stub.js.map +1 -1
- package/transpiled/directives/dev/Throw.d.ts +3 -3
- package/transpiled/directives/dev/Throw.js.map +1 -1
- package/transpiled/directives/dev/index.d.ts +2 -2
- package/transpiled/directives/dev/index.js +4 -5
- package/transpiled/directives/dev/index.js.map +1 -1
- package/transpiled/directives/dev/types.d.ts +1 -1
- package/transpiled/directives/index.d.ts +3 -1
- package/transpiled/directives/index.js +9 -9
- package/transpiled/directives/index.js.map +1 -1
- package/transpiled/directives/octets/Context.d.ts +1 -1
- package/transpiled/directives/octets/Delete.d.ts +1 -1
- package/transpiled/directives/octets/Delete.js.map +1 -1
- package/transpiled/directives/octets/Fetch.d.ts +1 -1
- package/transpiled/directives/octets/List.d.ts +1 -1
- package/transpiled/directives/octets/{Family.d.ts → Octets.d.ts} +3 -4
- package/transpiled/directives/octets/{Family.js → Octets.js} +4 -2
- package/transpiled/directives/octets/Octets.js.map +1 -0
- package/transpiled/directives/octets/Permute.d.ts +1 -1
- package/transpiled/directives/octets/Store.d.ts +1 -1
- package/transpiled/directives/octets/Store.js.map +1 -1
- package/transpiled/directives/octets/index.d.ts +2 -2
- package/transpiled/directives/octets/index.js +4 -5
- package/transpiled/directives/octets/index.js.map +1 -1
- package/transpiled/directives/octets/types.d.ts +3 -3
- package/transpiled/directives/vary/Directive.d.ts +5 -0
- package/transpiled/directives/vary/Directive.js +3 -0
- package/transpiled/directives/vary/Directive.js.map +1 -0
- package/transpiled/directives/vary/Embed.d.ts +10 -0
- package/transpiled/directives/vary/Embed.js +49 -0
- package/transpiled/directives/vary/Embed.js.map +1 -0
- package/transpiled/directives/vary/Properties.d.ts +9 -0
- package/transpiled/directives/vary/Properties.js +16 -0
- package/transpiled/directives/vary/Properties.js.map +1 -0
- package/transpiled/directives/vary/Vary.d.ts +10 -0
- package/transpiled/directives/vary/Vary.js +36 -0
- package/transpiled/directives/vary/Vary.js.map +1 -0
- package/transpiled/directives/vary/embeddings/Embedding.d.ts +5 -0
- package/transpiled/directives/vary/embeddings/Embedding.js +3 -0
- package/transpiled/directives/vary/embeddings/Embedding.js.map +1 -0
- package/transpiled/directives/vary/embeddings/Header.d.ts +7 -0
- package/transpiled/directives/vary/embeddings/Header.js +26 -0
- package/transpiled/directives/vary/embeddings/Header.js.map +1 -0
- package/transpiled/directives/vary/embeddings/Language.d.ts +7 -0
- package/transpiled/directives/vary/embeddings/Language.js +28 -0
- package/transpiled/directives/vary/embeddings/Language.js.map +1 -0
- package/transpiled/directives/vary/embeddings/index.d.ts +5 -0
- package/transpiled/directives/vary/embeddings/index.js +10 -0
- package/transpiled/directives/vary/embeddings/index.js.map +1 -0
- package/transpiled/directives/vary/index.d.ts +2 -0
- package/transpiled/directives/vary/index.js +6 -0
- package/transpiled/directives/vary/index.js.map +1 -0
- package/transpiled/io.d.ts +3 -0
- package/transpiled/io.js +3 -0
- package/transpiled/io.js.map +1 -0
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/transpiled/directives/auth/Family.js.map +0 -1
- package/transpiled/directives/dev/Family.js.map +0 -1
- package/transpiled/directives/octets/Family.js.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Feature: Identity Federation
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given the `identity.federation` database is empty
|
|
5
|
+
Given local IDP is running
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Scenario: Getting identity for a new user
|
|
9
|
+
Given the `identity.federation` configuration:
|
|
10
|
+
"""yaml
|
|
11
|
+
explicit_identity_creation: false
|
|
12
|
+
trust:
|
|
13
|
+
- issuer: http://localhost:44444
|
|
14
|
+
"""
|
|
15
|
+
And the IDP token for User is issued
|
|
16
|
+
When the following request is received:
|
|
17
|
+
"""
|
|
18
|
+
GET /identity/ HTTP/1.1
|
|
19
|
+
authorization: Bearer ${{ User.id_token }}
|
|
20
|
+
accept: application/yaml
|
|
21
|
+
content-type: application/yaml
|
|
22
|
+
"""
|
|
23
|
+
Then the following reply is sent:
|
|
24
|
+
"""
|
|
25
|
+
200 OK
|
|
26
|
+
authorization: Token ${{ User.token }}
|
|
27
|
+
|
|
28
|
+
id: ${{ User.id }}
|
|
29
|
+
roles: []
|
|
30
|
+
scheme: bearer
|
|
31
|
+
"""
|
|
32
|
+
# validate token
|
|
33
|
+
When the following request is received:
|
|
34
|
+
"""
|
|
35
|
+
GET /identity/ HTTP/1.1
|
|
36
|
+
accept: application/yaml
|
|
37
|
+
authorization: Token ${{ User.token }}
|
|
38
|
+
"""
|
|
39
|
+
Then the following reply is sent:
|
|
40
|
+
"""
|
|
41
|
+
200 OK
|
|
42
|
+
id: ${{ User.id }}
|
|
43
|
+
"""
|
|
44
|
+
# ensuring identity idemptotency
|
|
45
|
+
When the following request is received:
|
|
46
|
+
"""
|
|
47
|
+
GET /identity/ HTTP/1.1
|
|
48
|
+
authorization: Bearer ${{ User.id_token }}
|
|
49
|
+
accept: application/yaml
|
|
50
|
+
"""
|
|
51
|
+
Then the following reply is sent:
|
|
52
|
+
"""
|
|
53
|
+
200 OK
|
|
54
|
+
id: ${{ User.id }}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
Scenario: Creating an Identity using inception with existing credentials
|
|
58
|
+
Given the `identity.federation` configuration:
|
|
59
|
+
"""yaml
|
|
60
|
+
trust:
|
|
61
|
+
- issuer: http://localhost:44444
|
|
62
|
+
"""
|
|
63
|
+
Given the `users` is running with the following manifest:
|
|
64
|
+
"""yaml
|
|
65
|
+
exposition:
|
|
66
|
+
/:
|
|
67
|
+
anonymous: true
|
|
68
|
+
POST:
|
|
69
|
+
incept: id
|
|
70
|
+
endpoint: transit
|
|
71
|
+
"""
|
|
72
|
+
And the IDP token for Bill is issued
|
|
73
|
+
When the following request is received:
|
|
74
|
+
# identity inception
|
|
75
|
+
"""
|
|
76
|
+
POST /users/ HTTP/1.1
|
|
77
|
+
authorization: Bearer ${{ Bill.id_token }}
|
|
78
|
+
accept: application/yaml
|
|
79
|
+
content-type: application/yaml
|
|
80
|
+
|
|
81
|
+
name: Bill Smith
|
|
82
|
+
"""
|
|
83
|
+
Then the following reply is sent:
|
|
84
|
+
"""
|
|
85
|
+
201 Created
|
|
86
|
+
authorization: Token ${{ Bill.token }}
|
|
87
|
+
|
|
88
|
+
id: ${{ Bill.id }}
|
|
89
|
+
"""
|
|
90
|
+
# check that both tokens corresponds to the same id
|
|
91
|
+
When the following request is received:
|
|
92
|
+
"""
|
|
93
|
+
GET /identity/ HTTP/1.1
|
|
94
|
+
authorization: Token ${{ Bill.token }}
|
|
95
|
+
accept: application/yaml
|
|
96
|
+
"""
|
|
97
|
+
Then the following reply is sent:
|
|
98
|
+
"""
|
|
99
|
+
200 OK
|
|
100
|
+
id: ${{ Bill.id }}
|
|
101
|
+
"""
|
|
102
|
+
When the following request is received:
|
|
103
|
+
"""
|
|
104
|
+
GET /identity/ HTTP/1.1
|
|
105
|
+
authorization: Bearer ${{ Bill.id_token }}
|
|
106
|
+
accept: application/yaml
|
|
107
|
+
"""
|
|
108
|
+
Then the following reply is sent:
|
|
109
|
+
"""
|
|
110
|
+
200 OK
|
|
111
|
+
id: ${{ Bill.id }}
|
|
112
|
+
"""
|
|
113
|
+
And the following request is received:
|
|
114
|
+
# same credentials
|
|
115
|
+
"""
|
|
116
|
+
POST /users/ HTTP/1.1
|
|
117
|
+
authorization: Bearer ${{ Bill.id_token }}
|
|
118
|
+
content-type: text/plain
|
|
119
|
+
|
|
120
|
+
name: Mary Louis
|
|
121
|
+
"""
|
|
122
|
+
Then the following reply is sent:
|
|
123
|
+
"""
|
|
124
|
+
403 Forbidden
|
|
125
|
+
"""
|
|
Binary file
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as assert from 'node:assert'
|
|
1
2
|
import { after, binding, given } from 'cucumber-tsflow'
|
|
2
3
|
import * as boot from '@toa.io/boot'
|
|
3
4
|
import { timeout } from '@toa.io/generic'
|
|
@@ -30,9 +31,13 @@ export class Components {
|
|
|
30
31
|
@given('the `{word}` is stopped')
|
|
31
32
|
public async stop (_?: string): Promise<void> {
|
|
32
33
|
await this.composition?.disconnect()
|
|
34
|
+
|
|
35
|
+
this.composition = null
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
private async runComponent (name: string, manifest?: object): Promise<void> {
|
|
39
|
+
assert.ok(this.composition === null, 'Composition is already running')
|
|
40
|
+
|
|
36
41
|
const path = await this.workspace.addComponent(name, manifest)
|
|
37
42
|
|
|
38
43
|
this.composition = await boot.composition([path])
|
package/features/steps/HTTP.ts
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
|
+
import * as assert from 'node:assert'
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as path from 'node:path'
|
|
1
4
|
import { binding, then, when } from 'cucumber-tsflow'
|
|
2
5
|
import * as http from '@toa.io/http'
|
|
3
|
-
import
|
|
6
|
+
import * as msgpack from 'msgpackr'
|
|
7
|
+
import * as YAML from 'js-yaml'
|
|
8
|
+
import { Captures } from './Captures'
|
|
4
9
|
import { Parameters } from './Parameters'
|
|
5
10
|
import { Gateway } from './Gateway'
|
|
11
|
+
import type { Readable } from 'node:stream'
|
|
6
12
|
|
|
7
|
-
@binding([Gateway, Parameters])
|
|
13
|
+
@binding([Gateway, Parameters, Captures])
|
|
8
14
|
export class HTTP extends http.Agent {
|
|
9
15
|
private readonly gateway: Gateway
|
|
16
|
+
private fetched: Response | null = null
|
|
10
17
|
|
|
11
|
-
public constructor (gateway: Gateway, parameters: Parameters) {
|
|
12
|
-
super(parameters.origin)
|
|
18
|
+
public constructor (gateway: Gateway, parameters: Parameters, captures: Captures) {
|
|
19
|
+
super(parameters.origin, captures)
|
|
13
20
|
this.gateway = gateway
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
@when('the following request is received:')
|
|
17
24
|
public override async request (input: string): Promise<any> {
|
|
18
25
|
await this.gateway.start()
|
|
19
|
-
await super.request(input)
|
|
26
|
+
this.fetched = await super.request(input)
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
@then('the following reply is sent:')
|
|
@@ -24,6 +31,17 @@ export class HTTP extends http.Agent {
|
|
|
24
31
|
super.responseIncludes(expected)
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
@then('response body contains {word}-encoded value:')
|
|
35
|
+
public async bodyIs (format: string, yaml: string): Promise<void> {
|
|
36
|
+
assert.ok(this.fetched !== null, 'Response is null')
|
|
37
|
+
|
|
38
|
+
const buf = await this.fetched.arrayBuffer()
|
|
39
|
+
const value = encoders[format]?.(buf as Buffer)
|
|
40
|
+
const expected = YAML.load(yaml)
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(value, expected, 'Values are not equal')
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
@then('the reply does not contain:')
|
|
28
46
|
public override responseExcludes (expected: string): void {
|
|
29
47
|
super.responseExcludes(expected)
|
|
@@ -44,3 +62,13 @@ export class HTTP extends http.Agent {
|
|
|
44
62
|
await super.streamMatch(head, stream)
|
|
45
63
|
}
|
|
46
64
|
}
|
|
65
|
+
|
|
66
|
+
const FILEDIR = path.resolve(__dirname, '../../../storages/source/test')
|
|
67
|
+
|
|
68
|
+
function open (filename: string): Readable {
|
|
69
|
+
return fs.createReadStream(path.join(FILEDIR, filename))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const encoders: Record<string, (buf: Buffer | Uint8Array) => any> = {
|
|
73
|
+
MessagePack: msgpack.decode
|
|
74
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { once } from 'node:events'
|
|
2
|
+
import * as crypto from 'node:crypto'
|
|
3
|
+
import * as http from 'node:http'
|
|
4
|
+
import * as assert from 'node:assert'
|
|
5
|
+
import * as util from 'node:util'
|
|
6
|
+
import { binding, given, afterAll } from 'cucumber-tsflow'
|
|
7
|
+
import { Captures } from './Captures'
|
|
8
|
+
|
|
9
|
+
import type { AddressInfo } from 'node:net'
|
|
10
|
+
|
|
11
|
+
@binding([Captures])
|
|
12
|
+
export class IdP {
|
|
13
|
+
private static server?: http.Server
|
|
14
|
+
private static privateKey?: crypto.KeyObject
|
|
15
|
+
private static issuer?: string
|
|
16
|
+
|
|
17
|
+
public constructor (private readonly captures: Captures) {}
|
|
18
|
+
|
|
19
|
+
@afterAll()
|
|
20
|
+
public static async stop (): Promise<void> {
|
|
21
|
+
if (this.server instanceof http.Server) {
|
|
22
|
+
this.server.close()
|
|
23
|
+
await once(this.server, 'close')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@given(/local IDP is running/i)
|
|
28
|
+
public async start (): Promise<void> {
|
|
29
|
+
if (IdP.server instanceof http.Server) return
|
|
30
|
+
|
|
31
|
+
// creating the key
|
|
32
|
+
const { publicKey, privateKey } = await util.promisify(crypto.generateKeyPair)('rsa', {
|
|
33
|
+
modulusLength: 2048
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
IdP.privateKey = privateKey
|
|
37
|
+
|
|
38
|
+
const jwk = JSON.stringify({
|
|
39
|
+
keys: [{ use: 'sig', alg: 'RS256', ...publicKey.export({ format: 'jwk' }) }]
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const JWK_URL = '/.well-known/jwks'
|
|
43
|
+
|
|
44
|
+
const server = http.createServer((request, response) => {
|
|
45
|
+
switch (request.url) {
|
|
46
|
+
case JWK_URL:
|
|
47
|
+
response.writeHead(200, {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
'Content-Length': jwk.length,
|
|
50
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
51
|
+
Pragma: 'no-cache',
|
|
52
|
+
Expires: '0'
|
|
53
|
+
})
|
|
54
|
+
response.end(jwk)
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
case '/.well-known/openid-configuration':
|
|
58
|
+
{
|
|
59
|
+
const openIdConfiguration = JSON.stringify({
|
|
60
|
+
issuer: IdP.issuer,
|
|
61
|
+
jwks_uri: IdP.issuer + JWK_URL,
|
|
62
|
+
response_types_supported: ['id_token'],
|
|
63
|
+
subject_types_supported: ['public'],
|
|
64
|
+
id_token_signing_alg_values_supported: ['RS256'],
|
|
65
|
+
scopes_supported: ['openid']
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
response.writeHead(200, {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Cache-Control': 'public, max-age=3600',
|
|
71
|
+
'Content-Length': openIdConfiguration.length
|
|
72
|
+
})
|
|
73
|
+
response.end(openIdConfiguration)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
default:
|
|
79
|
+
response.writeHead(404, 'Not found')
|
|
80
|
+
response.end()
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
server.listen(44444, 'localhost')
|
|
85
|
+
await once(server, 'listening')
|
|
86
|
+
|
|
87
|
+
const address = server.address() as AddressInfo
|
|
88
|
+
|
|
89
|
+
console.log('IdP is listening on %s:%s', address.address, address.port)
|
|
90
|
+
IdP.server = server
|
|
91
|
+
IdP.issuer = `http://localhost:${address.port}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@given('the IDP token for {word} is issued')
|
|
95
|
+
public async issueToken (user: string): Promise<void> {
|
|
96
|
+
assert.ok(IdP.privateKey, 'IdP private key is not available')
|
|
97
|
+
|
|
98
|
+
const jwt = [
|
|
99
|
+
{
|
|
100
|
+
typ: 'JWT',
|
|
101
|
+
alg: 'RS256'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
iss: IdP.issuer,
|
|
105
|
+
sub: `${user}-mock-id`,
|
|
106
|
+
aud: 'test',
|
|
107
|
+
iat: Math.floor(Date.now() / 1000),
|
|
108
|
+
exp: Math.floor((Date.now() + 1000 * 60 * 5) / 1000)
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
.map((v) => Buffer.from(JSON.stringify(v)).toString('base64url'))
|
|
112
|
+
.join('.')
|
|
113
|
+
|
|
114
|
+
const signature = crypto.createSign('RSA-SHA256').end(jwt).sign(IdP.privateKey, 'base64url')
|
|
115
|
+
|
|
116
|
+
const idToken = `${jwt}.${signature}`
|
|
117
|
+
|
|
118
|
+
this.captures.set(`${user}.id_token`, idToken)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { setDefaultTimeout } from '@cucumber/cucumber'
|
|
2
|
+
import { encode } from '@toa.io/generic'
|
|
2
3
|
|
|
3
4
|
export class Parameters {
|
|
4
5
|
public readonly origin: string
|
|
@@ -9,7 +10,12 @@ export class Parameters {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
setDefaultTimeout(30 * 1000)
|
|
13
|
+
|
|
12
14
|
process.env.TOA_DEV = '1'
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
process.env.TOA_STORAGES = encode({
|
|
17
|
+
octets: {
|
|
18
|
+
provider: 'tmp',
|
|
19
|
+
prefix: 'test'
|
|
20
|
+
}
|
|
21
|
+
})
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { mkdtemp,
|
|
2
|
+
import { tmpdir, devNull } from 'node:os'
|
|
3
|
+
import { mkdtemp, cp } from 'node:fs/promises'
|
|
4
4
|
import * as yaml from '@toa.io/yaml'
|
|
5
5
|
|
|
6
6
|
export class Workspace {
|
|
7
|
-
private root: string =
|
|
7
|
+
private root: string = devNull
|
|
8
8
|
|
|
9
9
|
public static exists
|
|
10
10
|
(_0: unknown, _1: unknown, descriptor: PropertyDescriptor): PropertyDescriptor {
|
|
11
11
|
const method = descriptor.value
|
|
12
12
|
|
|
13
13
|
descriptor.value = async function (this: Workspace, ...args: any[]): Promise<any> {
|
|
14
|
-
if (this.root ===
|
|
14
|
+
if (this.root === devNull) this.root =
|
|
15
15
|
await mkdtemp(join(tmpdir(), Math.random().toString(36).slice(2)))
|
|
16
16
|
|
|
17
17
|
return method.apply(this, args)
|
|
@@ -25,7 +25,7 @@ export class Workspace {
|
|
|
25
25
|
const source = join(__dirname, 'components', name)
|
|
26
26
|
const target = join(this.root, name)
|
|
27
27
|
|
|
28
|
-
await
|
|
28
|
+
await cp(source, target, { force: true, recursive: true })
|
|
29
29
|
|
|
30
30
|
if (patch !== undefined)
|
|
31
31
|
await this.patchManifest(target, patch)
|
|
@@ -39,5 +39,3 @@ export class Workspace {
|
|
|
39
39
|
await yaml.patch(path, patch)
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
const devnull = '/dev/null'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Feature: The Vary directive family
|
|
2
|
+
|
|
3
|
+
Scenario Outline: Embedding a language code
|
|
4
|
+
Given the `echo` is running with the following manifest:
|
|
5
|
+
"""yaml
|
|
6
|
+
exposition:
|
|
7
|
+
/:
|
|
8
|
+
vary:languages: [en, fr]
|
|
9
|
+
GET:
|
|
10
|
+
vary:embed:
|
|
11
|
+
name: language # embed resolved language code as a `name` property of the operation input
|
|
12
|
+
endpoint: compute
|
|
13
|
+
"""
|
|
14
|
+
When the following request is received:
|
|
15
|
+
"""
|
|
16
|
+
GET /echo/ HTTP/1.1
|
|
17
|
+
accept: application/yaml
|
|
18
|
+
accept-language: <accept>
|
|
19
|
+
"""
|
|
20
|
+
Then the following reply is sent:
|
|
21
|
+
"""
|
|
22
|
+
200 OK
|
|
23
|
+
content-type: application/yaml
|
|
24
|
+
content-language: <result>
|
|
25
|
+
vary: accept-language, accept
|
|
26
|
+
|
|
27
|
+
Hello <result>
|
|
28
|
+
"""
|
|
29
|
+
Examples:
|
|
30
|
+
| accept | result |
|
|
31
|
+
| en | en |
|
|
32
|
+
| en_US | en |
|
|
33
|
+
| fr | fr |
|
|
34
|
+
| sw | en |
|
|
35
|
+
|
|
36
|
+
Scenario: Embedding a value of an arbitrary header
|
|
37
|
+
Given the `echo` is running with the following manifest:
|
|
38
|
+
"""yaml
|
|
39
|
+
exposition:
|
|
40
|
+
/:
|
|
41
|
+
GET:
|
|
42
|
+
vary:embed:
|
|
43
|
+
name: :foo
|
|
44
|
+
endpoint: compute
|
|
45
|
+
"""
|
|
46
|
+
When the following request is received:
|
|
47
|
+
"""
|
|
48
|
+
GET /echo/ HTTP/1.1
|
|
49
|
+
accept: application/yaml
|
|
50
|
+
foo: bar
|
|
51
|
+
"""
|
|
52
|
+
Then the following reply is sent:
|
|
53
|
+
"""
|
|
54
|
+
200 OK
|
|
55
|
+
content-type: application/yaml
|
|
56
|
+
vary: foo, accept
|
|
57
|
+
|
|
58
|
+
Hello bar
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
Scenario Outline: Embedding a value from the list of options
|
|
62
|
+
Given the `echo` is running with the following manifest:
|
|
63
|
+
"""yaml
|
|
64
|
+
exposition:
|
|
65
|
+
/:
|
|
66
|
+
vary:languages: [en, fr]
|
|
67
|
+
GET:
|
|
68
|
+
vary:embed:
|
|
69
|
+
name:
|
|
70
|
+
- :foo
|
|
71
|
+
- :bar
|
|
72
|
+
- language
|
|
73
|
+
endpoint: compute
|
|
74
|
+
"""
|
|
75
|
+
When the following request is received:
|
|
76
|
+
"""
|
|
77
|
+
GET /echo/ HTTP/1.1
|
|
78
|
+
accept: application/yaml
|
|
79
|
+
<header>: <value>
|
|
80
|
+
"""
|
|
81
|
+
Then the following reply is sent:
|
|
82
|
+
"""
|
|
83
|
+
200 OK
|
|
84
|
+
content-type: application/yaml
|
|
85
|
+
vary: <header>, accept
|
|
86
|
+
|
|
87
|
+
Hello <value>
|
|
88
|
+
"""
|
|
89
|
+
Examples:
|
|
90
|
+
| header | value |
|
|
91
|
+
| foo | bar |
|
|
92
|
+
| bar | baz |
|
|
93
|
+
| accept-language | en |
|
|
94
|
+
|
|
95
|
+
Scenario: Adding headers used by defined embeddings to CORS permissions
|
|
96
|
+
Given the `echo` is running with the following manifest:
|
|
97
|
+
"""yaml
|
|
98
|
+
exposition:
|
|
99
|
+
/:
|
|
100
|
+
vary:languages: [en, fr]
|
|
101
|
+
GET:
|
|
102
|
+
vary:embed:
|
|
103
|
+
name:
|
|
104
|
+
- language
|
|
105
|
+
- :foo
|
|
106
|
+
- :FOO
|
|
107
|
+
- :bar
|
|
108
|
+
endpoint: compute
|
|
109
|
+
"""
|
|
110
|
+
When the following request is received:
|
|
111
|
+
"""
|
|
112
|
+
OPTIONS / HTTP/1.1
|
|
113
|
+
origin: http://example.com
|
|
114
|
+
access-control-request-headers: whatever
|
|
115
|
+
"""
|
|
116
|
+
Then the following reply is sent:
|
|
117
|
+
"""
|
|
118
|
+
204 No Content
|
|
119
|
+
access-control-allow-headers: accept, content-type, accept-language, foo, bar
|
|
120
|
+
"""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "0.24.0-alpha.
|
|
3
|
+
"version": "0.24.0-alpha.19",
|
|
4
4
|
"description": "Toa Exposition",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -17,17 +17,16 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@toa.io/core": "0.24.0-alpha.
|
|
21
|
-
"@toa.io/generic": "0.24.0-alpha.
|
|
22
|
-
"@toa.io/schemas": "0.24.0-alpha.
|
|
23
|
-
"@toa.io/streams": "0.24.0-alpha.
|
|
20
|
+
"@toa.io/core": "0.24.0-alpha.19",
|
|
21
|
+
"@toa.io/generic": "0.24.0-alpha.19",
|
|
22
|
+
"@toa.io/schemas": "0.24.0-alpha.19",
|
|
23
|
+
"@toa.io/streams": "0.24.0-alpha.19",
|
|
24
24
|
"bcryptjs": "2.4.3",
|
|
25
|
-
"cors": "2.8.5",
|
|
26
25
|
"error-value": "0.3.0",
|
|
27
26
|
"express": "4.18.2",
|
|
28
27
|
"js-yaml": "4.1.0",
|
|
29
28
|
"matchacho": "0.3.5",
|
|
30
|
-
"msgpackr": "1.
|
|
29
|
+
"msgpackr": "1.10.1",
|
|
31
30
|
"negotiator": "0.6.3",
|
|
32
31
|
"paseto": "3.1.4"
|
|
33
32
|
},
|
|
@@ -37,21 +36,21 @@
|
|
|
37
36
|
},
|
|
38
37
|
"scripts": {
|
|
39
38
|
"test": "jest",
|
|
40
|
-
"transpile": "
|
|
41
|
-
"transpile:basic": "
|
|
42
|
-
"
|
|
43
|
-
"transpile:
|
|
44
|
-
"
|
|
39
|
+
"transpile": "tsc && npm run transpile:basic && npm run transpile:tokens && npm run transpile:roles && npm run transpile:federation",
|
|
40
|
+
"transpile:basic": "tsc -p ./components/identity.basic",
|
|
41
|
+
"pretranspile:federation": "js-yaml components/identity.federation/manifest.toa.yaml | jq -M '{ type: \"object\", properties: {configuration: .configuration.schema, entity: .entity.schema }, additionalProperties: false}' > schemas.json && json2ts -i schemas.json -o components/identity.federation/source/schemas.ts && rm schemas.json",
|
|
42
|
+
"transpile:federation": "tsc -p ./components/identity.federation",
|
|
43
|
+
"transpile:tokens": "tsc -p ./components/identity.tokens",
|
|
44
|
+
"transpile:roles": "tsc -p ./components/identity.roles",
|
|
45
|
+
"features": "cucumber-js"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
|
-
"@toa.io/extensions.storages": "0.24.0-alpha.
|
|
48
|
-
"@toa.io/http": "0.24.0-alpha.
|
|
48
|
+
"@toa.io/extensions.storages": "0.24.0-alpha.19",
|
|
49
|
+
"@toa.io/http": "0.24.0-alpha.19",
|
|
49
50
|
"@types/bcryptjs": "2.4.3",
|
|
50
51
|
"@types/cors": "2.8.13",
|
|
51
52
|
"@types/express": "4.17.17",
|
|
52
|
-
"@types/
|
|
53
|
-
"@types/negotiator": "0.6.1",
|
|
54
|
-
"fs-extra": "11.1.1"
|
|
53
|
+
"@types/negotiator": "0.6.1"
|
|
55
54
|
},
|
|
56
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "5e251e2f9ec49448c0a038d7aa07b01ba2b8d065"
|
|
57
56
|
}
|
package/source/Directive.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
1
2
|
import { generate } from 'randomstring'
|
|
2
3
|
import { DirectivesFactory, type Family } from './Directive'
|
|
3
4
|
import { type syntax } from './RTD'
|
|
4
5
|
import { type IncomingMessage } from './HTTP'
|
|
5
6
|
import { type Remotes } from './Remotes'
|
|
6
7
|
|
|
7
|
-
const families: Array<jest.
|
|
8
|
+
const families: Array<jest.MockedObjectDeep<Family>> = [
|
|
8
9
|
{
|
|
9
10
|
name: 'foo',
|
|
10
11
|
mandatory: true,
|
|
@@ -26,6 +27,9 @@ let factory: DirectivesFactory
|
|
|
26
27
|
beforeEach(() => {
|
|
27
28
|
jest.clearAllMocks()
|
|
28
29
|
|
|
30
|
+
assert.ok(families[0].preflight !== undefined)
|
|
31
|
+
assert.ok(families[1].preflight !== undefined)
|
|
32
|
+
|
|
29
33
|
families[0].preflight.mockImplementation(() => null)
|
|
30
34
|
families[1].preflight.mockImplementation(() => null)
|
|
31
35
|
factory = new DirectivesFactory(families, {} as unknown as Remotes)
|
|
@@ -61,7 +65,7 @@ it('should throw error if directive family is not found', async () => {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
expect(() => factory.create([declaration]))
|
|
64
|
-
.toThrowError(`Directive family '${declaration.family}' not found.`)
|
|
68
|
+
.toThrowError(`Directive family '${declaration.family}' is not found.`)
|
|
65
69
|
})
|
|
66
70
|
|
|
67
71
|
it('should apply directive', async () => {
|
|
@@ -77,6 +81,8 @@ it('should apply directive', async () => {
|
|
|
77
81
|
|
|
78
82
|
await directives.preflight(request, [])
|
|
79
83
|
|
|
84
|
+
assert.ok(families[0].preflight !== undefined)
|
|
85
|
+
|
|
80
86
|
expect(families[0].preflight.mock.calls[0][0]).toStrictEqual([directive])
|
|
81
87
|
expect(families[0].preflight.mock.calls[0][1]).toEqual(request)
|
|
82
88
|
})
|