Skip to content

Commit

Permalink
generated client code for policy api, implemented presentation_defini…
Browse files Browse the repository at this point in the history
…tion call
  • Loading branch information
woutslakhorst committed Nov 17, 2023
1 parent b829742 commit b878710
Show file tree
Hide file tree
Showing 7 changed files with 693 additions and 5 deletions.
11 changes: 11 additions & 0 deletions codegen/configs/policy_client_v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package: client
generate:
echo-server: false
client: true
models: true
strict-server: true
output-options:
skip-prune: true
exclude-schemas:
- PresentationDefinition
- PresentationSubmission
15 changes: 10 additions & 5 deletions docs/_static/policy/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ info:
servers:
- url: "http://localhost:1323"
paths:
/{did}/presentation_definition:
/presentation_definition:
parameters:
- name: did
in: path
- name: authorizer
in: query
description: URLEncoded DID.
required: true
example: did:web:example.com:1
Expand Down Expand Up @@ -52,7 +52,7 @@ paths:
When an access token is used to request a resource, the resource server needs to know if the access token grants access to the requested resource.
The resource server will send a request to the policy backend to check if the access token grants access to the requested resource.
All cryptographic and presentation exchange validations have already been done by the caller.
operationId: "authorized"
operationId: "checkAuthorized"
tags:
- policy
requestBody:
Expand Down Expand Up @@ -104,7 +104,7 @@ components:
type: string
presentation_submission:
description: The presentation submission that was used to request the access token.
type: object
$ref: '#/components/schemas/PresentationSubmission'
vps:
description: |
The verifiable presentations that were used to request the access token.
Expand All @@ -127,3 +127,8 @@ components:
A presentation definition is a JSON object that describes the desired verifiable credentials and presentation formats.
Specified at https://identity.foundation/presentation-exchange/spec/v2.0.0/
A JSON schema is available at https://identity.foundation/presentation-exchange/#json-schema
PresentationSubmission:
description: |
A presentation submission is a JSON object that maps requirements from the Presentation Definition to the verifiable presentations that were used to request an access token.
Specified at https://identity.foundation/presentation-exchange/spec/v2.0.0/
A JSON schema is available at https://identity.foundation/presentation-exchange/#json-schema
1 change: 1 addition & 0 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ gen-api:
oapi-codegen --config codegen/configs/auth_iam.yaml docs/_static/auth/iam.yaml | gofmt > auth/api/iam/generated.go
oapi-codegen --config codegen/configs/didman_v1.yaml docs/_static/didman/v1.yaml | gofmt > didman/api/v1/generated.go
oapi-codegen --config codegen/configs/crypto_store_client.yaml https://raw.githubusercontent.com/nuts-foundation/secret-store-api/main/nuts-storage-api-v1.yaml | gofmt > crypto/storage/external/generated.go
oapi-codegen --config codegen/configs/policy_client_v1.yaml docs/_static/policy/v1.yaml | gofmt > policy/api/v1/client/generated.go

gen-protobuf:
protoc --go_out=paths=source_relative:network -I network network/transport/v2/protocol.proto
Expand Down
83 changes: 83 additions & 0 deletions policy/api/v1/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package client

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/nuts-foundation/go-did/did"
"io"
"net/http"
"net/url"
"time"

"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/vcr/pe"
)

// HTTPClient holds the server address and other basic settings for the http client
type HTTPClient struct {
strictMode bool
httpClient core.HTTPRequestDoer
}

// NewHTTPClient creates a new api client.
func NewHTTPClient(strictMode bool, timeout time.Duration, tlsConfig *tls.Config) HTTPClient {
return HTTPClient{
strictMode: strictMode,
httpClient: core.NewStrictHTTPClient(strictMode, timeout, tlsConfig),
}
}

// PresentationDefinition retrieves the presentation definition from the presentation definition endpoint for the given scope and .
func (hb HTTPClient) PresentationDefinition(ctx context.Context, policyEndpoint string, authorizer did.DID, scopes string) (*pe.PresentationDefinition, error) {
presentationDefinitionURL, err := core.ParsePublicURL(policyEndpoint, hb.strictMode)
if err != nil {
return nil, err
}
presentationDefinitionURL.Path = fmt.Sprintf("%s/presentation_definition", presentationDefinitionURL.Path)
presentationDefinitionURL.RawQuery = url.Values{"scope": []string{scopes}, "authorizer": []string{authorizer.String()}}.Encode()

// create a GET request with query params
request, err := http.NewRequestWithContext(ctx, http.MethodGet, presentationDefinitionURL.String(), nil)
if err != nil {
return nil, err
}
response, err := hb.httpClient.Do(request.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("failed to call endpoint: %w", err)
}
if httpErr := core.TestResponseCode(http.StatusOK, response); httpErr != nil {
return nil, httpErr
}

var presentationDefinition pe.PresentationDefinition
var data []byte

if data, err = io.ReadAll(response.Body); err != nil {
return nil, fmt.Errorf("unable to read response: %w", err)
}
if err = json.Unmarshal(data, &presentationDefinition); err != nil {
return nil, fmt.Errorf("unable to unmarshal response: %w", err)
}

return &presentationDefinition, nil
}
106 changes: 106 additions & 0 deletions policy/api/v1/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2023 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package client

import (
"context"
"encoding/json"
"github.com/nuts-foundation/go-did/did"
http2 "github.com/nuts-foundation/nuts-node/test/http"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"testing"
)

func TestHTTPClient_PresentationDefinition(t *testing.T) {
ctx := context.Background()
authorizer := did.MustParseDID("did:web:example.com")
definition := pe.PresentationDefinition{
Id: "123",
}

t.Run("ok", func(t *testing.T) {
var capturedRequest *http.Request
handler := func(writer http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/presentation_definition":
capturedRequest = request
writer.WriteHeader(http.StatusOK)
bytes, _ := json.Marshal(definition)
writer.Write(bytes)
}
writer.WriteHeader(http.StatusNotFound)
}
tlsServer, client := testServerAndClient(t, http.HandlerFunc(handler))

response, err := client.PresentationDefinition(ctx, tlsServer.URL, authorizer, "test")

require.NoError(t, err)
require.NotNil(t, definition)
assert.Equal(t, definition, *response)
require.NotNil(t, capturedRequest)
assert.Equal(t, "GET", capturedRequest.Method)
assert.Equal(t, "/presentation_definition", capturedRequest.URL.Path)
// check query params
require.NotNil(t, capturedRequest.URL.Query().Get("scope"))
assert.Equal(t, "test", capturedRequest.URL.Query().Get("scope"))
require.NotNil(t, capturedRequest.URL.Query().Get("authorizer"))
assert.Equal(t, authorizer.String(), capturedRequest.URL.Query().Get("authorizer"))
})
t.Run("error - not found", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
tlsServer, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, tlsServer.URL, authorizer, "test")

require.Error(t, err)
assert.EqualError(t, err, "server returned HTTP 404 (expected: 200)")
assert.Nil(t, response)
})
t.Run("error - invalid URL", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
_, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, ":", authorizer, "test")

require.Error(t, err)
assert.EqualError(t, err, "parse \":\": missing protocol scheme")
assert.Nil(t, response)
})
t.Run("error - invalid response", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: "}"}
tlsServer, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, tlsServer.URL, authorizer, "test")

require.Error(t, err)
assert.EqualError(t, err, "unable to unmarshal response: invalid character '}' looking for beginning of value")
assert.Nil(t, response)
})
}

func testServerAndClient(t *testing.T, handler http.Handler) (*httptest.Server, *HTTPClient) {
tlsServer := http2.TestTLSServer(t, handler)
return tlsServer, &HTTPClient{
httpClient: tlsServer.Client(),
}
}
Loading

0 comments on commit b878710

Please sign in to comment.