Skip to content

Commit a974b29

Browse files
authored
Merge pull request #21 from okp4/feat/wire-orchestration
Feat/wire orchestration
2 parents bbf701d + a254e0e commit a974b29

36 files changed

+939
-71
lines changed

README.md

+149
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,152 @@
2222
```sh
2323
make build
2424
```
25+
26+
## Example
27+
28+
Hereafter is presented an example using this proxy locally, providing all the needed elements to feed a local dataverse and interact with it.
29+
30+
Through this example, we'll have a [Minio](https://github.com/minio/minio) instance declared as a digital storage service with an attached governance allowing usage in a specific zone. And a dataset representing a single file with a governance allowing the same zone and a specific orchestration service, the dataset will use the minio as storage service.
31+
32+
We'll see how we can submit an execution order to set the file accessible through the proxy by being authenticated as the orchestration service.
33+
34+
### Prerequistes
35+
36+
Some tools are needed in order to run the example:
37+
38+
- [docker](https://docs.docker.com/engine/install/)
39+
- [okp4d](https://github.com/okp4/okp4d)
40+
- [jsonld](https://github.com/digitalbazaar/jsonld-cli)
41+
42+
The local chain must be running with our [contracts](https://github.com/okp4/contracts) stored.
43+
44+
The local configuration of `okp4d` in `$OKP4D_HOME/config/client.toml` shall be self-sufficient to sign and broadcast transaction without additional command flags (e.g. `--chain-id`, `--keyring-backend`, etc..)
45+
46+
### Steps
47+
48+
#### Instantiate Smart contracts
49+
50+
For each contract instantiation, keep the contract addresses, as they will be required for future interactions. You can inspect the transaction hash generated by the broadcasting process with `okp4d query tx $TX_HASH` and look for the events section.
51+
52+
Let's begin with the objectarium:
53+
54+
```bash
55+
okp4d tx wasm instantiate $OBJECTARIUM_CODE_ID \
56+
--label "my-prologtarium" \
57+
--from $MY_WALLET_ADDR \
58+
--admin $MY_WALLET_ADDR \
59+
--gas 1000000 \
60+
'{"bucket":"my-prologtarium"}'
61+
```
62+
63+
Now let's create the law-stones containing the minio & dataset prolog governance codes:
64+
65+
```bash
66+
okp4d tx wasm instantiate $LAW_STONE_CODE_ID \
67+
--label "minio-gov" \
68+
--from local \
69+
--admin local \
70+
--gas 100000000 \
71+
"{\"program\":\"$(cat example/s3-gov.pl | base64)\", \"storage_address\": \"$OBJECTARIUM_ADDR\"}"
72+
okp4d tx wasm instantiate 2 \
73+
--label "data-gov" \
74+
--from local \
75+
--admin local \
76+
--gas 100000000 \
77+
"{\"program\":\"$(cat example/data-gov.pl | base64)\", \"storage_address\": \"$OBJECTARIUM_ADDR\"}"
78+
```
79+
80+
Finally, the dataverse:
81+
82+
```bash
83+
okp4d tx wasm instantiate $DATAVERSE_CODE_ID \
84+
--label "my-local-dataverse" \
85+
--from $MY_WALLET_ADDR \
86+
--admin $MY_WALLET_ADDR \
87+
--gas 1000000 \
88+
"{\"name\":\"my-local-dataverse\",\"triplestore_config\":{\"code_id\":\"$COGNITARIUM_CODE_ID\",\"limits\":{}}}"
89+
```
90+
91+
#### Declare resources
92+
93+
Now let's declare the storage service and the dataset in the dataverse: we'll have for each one two verifiable credentials, one for the description and one referencing the governance. Then, another one will be needed to express that the dataset is served by our minio storage service, providing its protected proxy URL. Those verifiable credentials are available here:
94+
95+
- [example/vc-s3-desc.jsonld]
96+
- [example/vc-s3-gov.jsonld]
97+
- [example/vc-data-desc.jsonld]
98+
- [example/vc-data-gov.jsonld]
99+
- [example/vc-publish.jsonld]
100+
101+
Before submitting them we need to update the law stone addresses related to the governances in the [example/vc-s3-gov.jsonld] and [example/vc-data-gov.jsonld] credentials.
102+
103+
Those VCs are not signed. For that we'll need to have some cryptographic keys to act as the issuers of those verifiable credentials. To facilitate this, we provide a keyring located at [example/keyring-test].
104+
You can list the keys with `okp4d --keyring-backend test --keyring-dir example keys list` if needed.
105+
106+
To sign and submit the verifiable credentials we have a simple script that you can use:
107+
108+
```bash
109+
./scripts/setup.sh $MY_WALLET_ADDR $DATAVERSE_ADDR
110+
```
111+
112+
#### Run the infrastructure
113+
114+
Here we need to run the minio and deploy our dataset on it. For that, we provide a [docker-compose.yml]: it will run a MinIO instance accessible at `http://localhost:9000`.For demonstration purposes, this setup will make the `README` file of this project available as part of the dataset at `http://localhost:9000/test/README.md`.
115+
You can start the compose with:
116+
117+
```bash
118+
docker compose up
119+
```
120+
121+
Now we'll run the proxy through which we'll connect to the dataverse with:
122+
123+
```bash
124+
./target/dist/s3-auth-proxy start --listen-addr 0.0.0.0:8080 \
125+
--jwt-secret-key 1d5be173d43385b984ef8c73fe4fb9e5ca5a31466f20bf8a250d06eec5f3079b \
126+
--s3-endpoint localhost:9000 \
127+
--s3-access-key minioadmin \
128+
--s3-secret-key minioadmin \
129+
--s3-insecure \
130+
--grpc-no-tls \
131+
--dataverse-addr $DATAVERSE_ADDR \
132+
--svc-id did:key:zQ3shbn6v6Mwtc6nSe5LnBmBY44seFqdRKXtf5eH8tQknZCcw
133+
```
134+
135+
#### Order an execution
136+
137+
For this step, we'll act ourselves as the initiator of the execution order, and the orchestration service that'll fulfill the order, to demonstrate the interactions with the proxy.
138+
139+
Let's create the execution order, and the execution containing the status and the parameters:
140+
141+
```bash
142+
./scripts/order-exec.sh $MY_WALLET_ADDR $DATAVERSE_ADDR
143+
```
144+
145+
#### Access the dataset
146+
147+
At this point, submitting an authentication verifiable credential signed with the orchestration service keys we should be able to access the dataset, let's forge this credential:
148+
149+
```bash
150+
./scripts/issue-auth-cred.sh > vc-auth.jsonld
151+
```
152+
153+
And then issue an authentication request to obtain an access token:
154+
155+
```bash
156+
curl -s -X POST -T ./vc-auth.jsonld http://localhost:8080/auth
157+
```
158+
159+
Now we should be able to get through the proxy authorization layer with our access token:
160+
161+
```bash
162+
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/test/README.md
163+
```
164+
165+
#### Terminate the execution
166+
167+
We just need to submit a credential expressing an execution status of delivered:
168+
169+
```bash
170+
./scripts/end-exec.sh $MY_WALLET_ADDR $DATAVERSE_ADDR
171+
```
172+
173+
At this point we're not anymore capable to access the dataset.

app/handler.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func makeProxyHandler(s3Client *minio.Client, authenticator *auth.Authenticator)
4848
return
4949
}
5050

51-
claims, err := authenticator.Authorize(authHeader[7:])
51+
claims, err := authenticator.Authorize(authHeader[7:], ctx.Request.URI())
5252
if err != nil {
5353
ctx.Response.SetStatusCode(fasthttp.StatusUnauthorized)
5454
ctx.Response.SetBody([]byte(err.Error()))

auth/authenticator.go

+79-11
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package auth
33
import (
44
"context"
55
"fmt"
6+
"net/url"
7+
"slices"
8+
9+
"github.com/valyala/fasthttp"
610

711
"okp4/s3-auth-proxy/dataverse"
812

9-
"github.com/golang-jwt/jwt"
1013
"github.com/piprate/json-gold/ld"
1114
)
1215

@@ -34,28 +37,93 @@ func (a *Authenticator) Authenticate(ctx context.Context, raw []byte) (string, e
3437
return "", fmt.Errorf("couldn't parse VC: %w", err)
3538
}
3639

37-
zone, err := a.dataverseClient.GetExecutionOrderContext(ctx, claim.ForOrder, claim.ID)
40+
execCtx, err := a.dataverseClient.GetExecutionOrderContext(ctx, claim.ForOrder, claim.ID)
3841
if err != nil {
3942
return "", fmt.Errorf("couldn't fetch execution order context: %w", err)
4043
}
4144

42-
govCode, err := a.dataverseClient.GetResourceGovCode(ctx, a.serviceID)
43-
if err != nil {
44-
return "", fmt.Errorf("couldn't get governance code: %w", err)
45+
executions := execCtx.ExecutionsInProgress()
46+
if len(executions) == 0 {
47+
return "", fmt.Errorf("execution order not in progress")
48+
}
49+
50+
var resources []string
51+
for _, exec := range executions {
52+
consumed, err := a.dataverseClient.GetExecutionConsumedResources(ctx, claim.ForOrder, exec)
53+
if err != nil {
54+
return "", fmt.Errorf("couldn't fetch execution consumed resources: %w", err)
55+
}
56+
57+
if !slices.Contains(consumed, a.serviceID) {
58+
continue
59+
}
60+
resources = append(resources, consumed...)
4561
}
4662

47-
res, err := a.dataverseClient.ExecGovernance(ctx, govCode, "service:use", claim.ID, zone)
63+
if len(resources) == 0 {
64+
return "", fmt.Errorf("not concerned by this execution order")
65+
}
66+
67+
res, err := a.execGovernance(ctx, a.serviceID, "service:use", claim.ID, execCtx.Zone)
4868
if err != nil {
49-
return "", fmt.Errorf("couldn't check governance: %w", err)
69+
return "", err
5070
}
5171
if res.Result != "permitted" {
52-
return "", fmt.Errorf("governance rejected access, evidence: %s", res.Evidence)
72+
return "", fmt.Errorf("access rejected by governance, evidence: %s", res.Evidence)
73+
}
74+
75+
resourcePublications := make([]string, 0)
76+
for _, r := range resources {
77+
if r == a.serviceID {
78+
continue
79+
}
80+
81+
uri, err := a.dataverseClient.GetResourcePublication(ctx, r, a.serviceID)
82+
if err != nil {
83+
return "", fmt.Errorf("couldn't fetch resource publication: %w", err)
84+
}
85+
if uri == nil {
86+
continue
87+
}
88+
89+
res, err := a.execGovernance(ctx, r, "dataset:read", claim.ID, execCtx.Zone)
90+
if err != nil {
91+
return "", err
92+
}
93+
if res.Result != "permitted" {
94+
return "", fmt.Errorf("access rejected by governance, evidence: %s", res.Evidence)
95+
}
96+
resourcePublications = append(resourcePublications, *uri)
5397
}
5498

55-
return a.issueJwt(claim.ID)
99+
return a.issueJwt(claim.ID, resourcePublications)
56100
}
57101

58102
// Authorize verifies the provided jwt access token.
59-
func (a *Authenticator) Authorize(raw string) (*jwt.StandardClaims, error) {
60-
return a.verifyJwt(raw)
103+
func (a *Authenticator) Authorize(token string, uri *fasthttp.URI) (*ProxyClaims, error) {
104+
claims, err := a.verifyJwt(token)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
if !claims.CanRead(func(raw string) bool {
110+
parsedURI, err := url.Parse(raw)
111+
return err != nil && parsedURI.Host == string(uri.Host()) && parsedURI.Path == string(uri.Path())
112+
}) {
113+
return claims, fmt.Errorf("access to requested resource unauthorized")
114+
}
115+
return claims, nil
116+
}
117+
118+
func (a *Authenticator) execGovernance(ctx context.Context, resource, action, subject, zone string) (*dataverse.GovernanceExecAnswer, error) {
119+
govCode, err := a.dataverseClient.GetResourceGovCode(ctx, resource)
120+
if err != nil {
121+
return nil, fmt.Errorf("couldn't fetch governance code: %w", err)
122+
}
123+
124+
res, err := a.dataverseClient.ExecGovernance(ctx, govCode, action, subject, zone)
125+
if err != nil {
126+
return nil, fmt.Errorf("couldn't exec governance: %w", err)
127+
}
128+
return res, nil
61129
}

auth/jwt.go

+35-12
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,44 @@ import (
88
"github.com/google/uuid"
99
)
1010

11-
func (a *Authenticator) issueJwt(authenticatedSvc string) (string, error) {
11+
type ProxyClaims struct {
12+
jwt.StandardClaims
13+
Can Permissions `json:"can"`
14+
}
15+
16+
type Permissions struct {
17+
Read []string `json:"read"`
18+
}
19+
20+
func (c *ProxyClaims) CanRead(canFn func(uri string) bool) bool {
21+
for _, u := range c.Can.Read {
22+
if canFn(u) {
23+
return true
24+
}
25+
}
26+
return false
27+
}
28+
29+
func (a *Authenticator) issueJwt(authenticatedSvc string, readURIs []string) (string, error) {
1230
now := time.Now()
13-
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
14-
Audience: authenticatedSvc,
15-
ExpiresAt: now.Add(5 * time.Minute).Unix(),
16-
Id: uuid.New().String(),
17-
IssuedAt: now.Unix(),
18-
Issuer: a.serviceID,
19-
NotBefore: now.Unix(),
20-
Subject: a.serviceID,
31+
return jwt.NewWithClaims(jwt.SigningMethodHS256, ProxyClaims{
32+
StandardClaims: jwt.StandardClaims{
33+
Audience: authenticatedSvc,
34+
ExpiresAt: now.Add(5 * time.Minute).Unix(),
35+
Id: uuid.New().String(),
36+
IssuedAt: now.Unix(),
37+
Issuer: a.serviceID,
38+
NotBefore: now.Unix(),
39+
Subject: authenticatedSvc,
40+
},
41+
Can: Permissions{
42+
Read: readURIs,
43+
},
2144
}).SignedString(a.jwtSecretKey)
2245
}
2346

24-
func (a *Authenticator) verifyJwt(raw string) (*jwt.StandardClaims, error) {
25-
token, err := jwt.ParseWithClaims(raw, &jwt.StandardClaims{}, func(_ *jwt.Token) (interface{}, error) {
47+
func (a *Authenticator) verifyJwt(raw string) (*ProxyClaims, error) {
48+
token, err := jwt.ParseWithClaims(raw, &ProxyClaims{}, func(_ *jwt.Token) (interface{}, error) {
2649
return a.jwtSecretKey, nil
2750
})
2851
if err != nil {
@@ -33,7 +56,7 @@ func (a *Authenticator) verifyJwt(raw string) (*jwt.StandardClaims, error) {
3356
return nil, fmt.Errorf("invalid token")
3457
}
3558

36-
c, ok := token.Claims.(*jwt.StandardClaims)
59+
c, ok := token.Claims.(*ProxyClaims)
3760
if !ok {
3861
return nil, fmt.Errorf("invalid claims")
3962
}

0 commit comments

Comments
 (0)