From 7dc0954f20758ebf3464346d3407eec3966b57eb Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 1 Oct 2024 11:23:15 +0200 Subject: [PATCH 01/72] fix http status codes for did subject creation (#3427) --- docs/_static/vdr/v2.yaml | 1 + vdr/api/v2/api.go | 18 ++++++++++-------- vdr/didsubject/interface.go | 12 ++++++++++++ vdr/didsubject/manager.go | 20 ++++++++++---------- vdr/didsubject/manager_test.go | 12 +++++++++--- 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/docs/_static/vdr/v2.yaml b/docs/_static/vdr/v2.yaml index f5ef50d75..9a254a444 100644 --- a/docs/_static/vdr/v2.yaml +++ b/docs/_static/vdr/v2.yaml @@ -131,6 +131,7 @@ paths: error returns: * 400 - the subject param was malformed * 404 - Corresponding subject could not be found + * 409 - The subject is already deactivated * 500 - An error occurred while processing the request operationId: "deactivate" tags: diff --git a/vdr/api/v2/api.go b/vdr/api/v2/api.go index cb68f04bc..d1e3c6950 100644 --- a/vdr/api/v2/api.go +++ b/vdr/api/v2/api.go @@ -56,13 +56,16 @@ type Wrapper struct { // ResolveStatusCode maps errors returned by this API to specific HTTP status codes. func (w *Wrapper) ResolveStatusCode(err error) int { return core.ResolveStatusCode(err, map[error]int{ - didsubject.ErrSubjectNotFound: http.StatusNotFound, - didsubject.ErrSubjectAlreadyExists: http.StatusConflict, - resolver.ErrNotFound: http.StatusNotFound, - resolver.ErrDIDNotManagedByThisNode: http.StatusForbidden, - did.ErrInvalidDID: http.StatusBadRequest, - didsubject.ErrInvalidService: http.StatusBadRequest, - didsubject.ErrUnsupportedDIDMethod: http.StatusBadRequest, + didsubject.ErrSubjectNotFound: http.StatusNotFound, + didsubject.ErrSubjectAlreadyExists: http.StatusConflict, + resolver.ErrNotFound: http.StatusNotFound, + resolver.ErrDIDNotManagedByThisNode: http.StatusForbidden, + did.ErrInvalidDID: http.StatusBadRequest, + didsubject.ErrInvalidService: http.StatusBadRequest, + didsubject.ErrUnsupportedDIDMethod: http.StatusBadRequest, + didsubject.ErrKeyAgreementNotSupported: http.StatusBadRequest, + didsubject.ErrSubjectValidation: http.StatusBadRequest, + resolver.ErrDeactivated: http.StatusConflict, }) } @@ -121,7 +124,6 @@ func (w *Wrapper) CreateSubject(ctx context.Context, request CreateSubjectReques } docs, subject, err := w.SubjectManager.Create(ctx, options) - // if this operation leads to an error, it may return a 500 if err != nil { return nil, err } diff --git a/vdr/didsubject/interface.go b/vdr/didsubject/interface.go index fe5e28abb..0c7d17c91 100644 --- a/vdr/didsubject/interface.go +++ b/vdr/didsubject/interface.go @@ -33,6 +33,18 @@ var ErrInvalidService = errors.New("invalid DID document service") // ErrUnsupportedDIDMethod is returned when a DID method is not supported. var ErrUnsupportedDIDMethod = errors.New("unsupported DID method") +// ErrKeyAgreementNotSupported is returned key agreement is required for did:web. +var ErrKeyAgreementNotSupported = errors.New("key agreement not supported for did:web") + +// ErrSubjectValidation is returned when the subject creation request is invalid. +var ErrSubjectValidation = errors.New("subject creation validation error") + +// ErrSubjectAlreadyExists is returned when a subject already exists. +var ErrSubjectAlreadyExists = errors.New("subject already exists") + +// ErrSubjectNotFound is returned when a subject is not found. +var ErrSubjectNotFound = errors.New("subject not found") + // MethodManager keeps DID method specific state in sync with the DID sql database. type MethodManager interface { // NewDocument generates a new DID document for the given subject. diff --git a/vdr/didsubject/manager.go b/vdr/didsubject/manager.go index ebdd95377..1888ac952 100644 --- a/vdr/didsubject/manager.go +++ b/vdr/didsubject/manager.go @@ -40,12 +40,6 @@ import ( "time" ) -// ErrSubjectAlreadyExists is returned when a subject already exists. -var ErrSubjectAlreadyExists = errors.New("subject already exists") - -// ErrSubjectNotFound is returned when a subject is not found. -var ErrSubjectNotFound = errors.New("subject not found") - // subjectPattern is a regular expression for checking whether a subject follows the allowed pattern; a-z, 0-9, -, _, . (case insensitive) var subjectPattern = regexp.MustCompile(`^[a-zA-Z0-9.-]+$`) @@ -125,7 +119,7 @@ func (r *SqlManager) Create(ctx context.Context, options CreationOptions) ([]did switch opt := option.(type) { case SubjectCreationOption: if !subjectPattern.MatchString(opt.Subject) { - return nil, "", fmt.Errorf("invalid subject (must follow pattern: %s)", subjectPattern.String()) + return nil, "", errors.Join(ErrSubjectValidation, fmt.Errorf("invalid subject (must follow pattern: %s)", subjectPattern.String())) } subject = opt.Subject case EncryptionKeyCreationOption: @@ -133,7 +127,7 @@ func (r *SqlManager) Create(ctx context.Context, options CreationOptions) ([]did case NutsLegacyNamingOption: nutsLegacy = true default: - return nil, "", fmt.Errorf("unknown option: %T", option) + return nil, "", errors.Join(ErrSubjectValidation, fmt.Errorf("unknown option: %T", option)) } } @@ -153,6 +147,12 @@ func (r *SqlManager) Create(ctx context.Context, options CreationOptions) ([]did // call generate on all managers for method, manager := range r.MethodManagers { + // known limitation, check is also done within the manager, but at this point we can return a known error for the API + // requires update to nutsCrypto module + if keyFlags.Is(orm.KeyAgreementUsage) && method == "web" { + return nil, ErrKeyAgreementNotSupported + } + // save tx in context to pass all the way down to KeyStore transactionContext := context.WithValue(ctx, storage.TransactionKey{}, tx) sqlDoc, err := manager.NewDocument(transactionContext, keyFlags) @@ -394,8 +394,8 @@ func (r *SqlManager) AddVerificationMethod(ctx context.Context, subject string, err := r.applyToDIDDocuments(ctx, subject, func(tx *gorm.DB, id did.DID, current *orm.DidDocument) (*orm.DidDocument, error) { // known limitation if keyUsage.Is(orm.KeyAgreementUsage) && id.Method == "web" { - return nil, errors.New("key agreement not supported for did:web") - // todo requires update to nutsCrypto module + return nil, ErrKeyAgreementNotSupported + // requires update to nutsCrypto module //verificationMethodKey, err = m.keyStore.NewRSA(ctx, func(key crypt.PublicKey) (string, error) { // return verificationMethodID.String(), nil //}) diff --git a/vdr/didsubject/manager_test.go b/vdr/didsubject/manager_test.go index f2746a6c7..a46b4d8e9 100644 --- a/vdr/didsubject/manager_test.go +++ b/vdr/didsubject/manager_test.go @@ -141,7 +141,9 @@ func TestManager_Create(t *testing.T) { _, _, err := m.Create(audit.TestContext(), DefaultCreationOptions().With("")) - require.EqualError(t, err, "unknown option: string") + require.Error(t, err) + assert.ErrorIs(t, err, ErrSubjectValidation) + assert.ErrorContains(t, err, "unknown option: string") }) t.Run("already exists", func(t *testing.T) { db := testDB(t) @@ -163,7 +165,9 @@ func TestManager_Create(t *testing.T) { _, _, err := m.Create(audit.TestContext(), DefaultCreationOptions().With(SubjectCreationOption{Subject: ""})) - require.EqualError(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9.-]+$)") + require.Error(t, err) + assert.ErrorIs(t, err, ErrSubjectValidation) + assert.ErrorContains(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9.-]+$)") }) t.Run("contains illegal character (space)", func(t *testing.T) { db := testDB(t) @@ -173,7 +177,9 @@ func TestManager_Create(t *testing.T) { _, _, err := m.Create(audit.TestContext(), DefaultCreationOptions().With(SubjectCreationOption{Subject: "subject with space"})) - require.EqualError(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9.-]+$)") + require.Error(t, err) + assert.ErrorIs(t, err, ErrSubjectValidation) + assert.ErrorContains(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9.-]+$)") }) }) } From e2ce9e0d4bbad502540b6177c9ff792ed51b5681 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:22:39 +0200 Subject: [PATCH 02/72] Migrate owned did nuts to SQL (#3392) * migrate did:nuts documents to SQL * add e2e-test setup * test migration hack is safe for production network * fix broken tests * reduce complexity * dont migrate documents past deactivation * fix merge conflict * generalize methods * add tests * e2e test; add SQL db checks * e2e test; add more SQL db checks * pr feedback + documentation * revert e2e-test * add e2e-test --- .github/workflows/e2e-tests.yaml | 2 +- .../integrating/version-incompatibilities.rst | 8 + docs/pages/release_notes.rst | 1 + .../docker-compose-post-migration.yml | 32 +++ .../docker-compose-pre-migration.yml | 32 +++ e2e-tests/migration/main_test.go | 120 +++++++++++ e2e-tests/migration/nuts-v5.yaml | 12 ++ e2e-tests/migration/nuts-v6.yaml | 15 ++ e2e-tests/migration/run-test.sh | 160 +++++++++++++++ e2e-tests/migration/run-tests.sh | 8 + e2e-tests/run-tests.sh | 7 + e2e-tests/util.sh | 7 +- storage/orm/did_document.go | 65 ++++++ storage/orm/did_document_test.go | 67 +++++- storage/orm/keyflag.go | 23 +++ storage/orm/keyflag_test.go | 41 ++++ storage/test.go | 2 + vdr/didnuts/didstore/interface.go | 6 + vdr/didnuts/didstore/mock.go | 16 ++ vdr/didnuts/didstore/store.go | 60 ++++++ vdr/didnuts/didstore/store_test.go | 75 +++++++ vdr/didsubject/did_document.go | 2 +- vdr/didsubject/interface.go | 8 + vdr/didsubject/manager.go | 77 ++++++- vdr/didsubject/manager_test.go | 112 ++++++++++ vdr/vdr.go | 117 +++++++---- vdr/vdr_test.go | 192 ++++++++++-------- 27 files changed, 1142 insertions(+), 125 deletions(-) create mode 100644 e2e-tests/migration/docker-compose-post-migration.yml create mode 100644 e2e-tests/migration/docker-compose-pre-migration.yml create mode 100644 e2e-tests/migration/main_test.go create mode 100644 e2e-tests/migration/nuts-v5.yaml create mode 100644 e2e-tests/migration/nuts-v6.yaml create mode 100755 e2e-tests/migration/run-test.sh create mode 100755 e2e-tests/migration/run-tests.sh diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index e77e48599..91ecbd7d3 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -75,7 +75,7 @@ jobs: - name: Run E2E tests run: | cd e2e-tests && \ - find . -type f -name "docker-compose.yml" | xargs -I{} sed -i 's~nutsfoundation/nuts-node:master~ghcr.io/nuts-foundation/nuts-node-ci:${{ env.SHA }}~g' {} && \ + find . -type f -name "docker-compose*.yml" | xargs -I{} sed -i 's~nutsfoundation/nuts-node:master~ghcr.io/nuts-foundation/nuts-node-ci:${{ env.SHA }}~g' {} && \ find . -type f -name "run-test.sh" | xargs -I{} sed -i 's/docker-compose exec/docker-compose exec -T/g' {} && \ ./run-tests.sh diff --git a/docs/pages/integrating/version-incompatibilities.rst b/docs/pages/integrating/version-incompatibilities.rst index 189333b6d..659e39d82 100644 --- a/docs/pages/integrating/version-incompatibilities.rst +++ b/docs/pages/integrating/version-incompatibilities.rst @@ -17,3 +17,11 @@ There are basically two options. Do not use the VDR V1 and VDR V2 API at the same time. This will lead to unexpected behavior. Once you use the VDR V2 API, you cannot go back to the VDR V1 API. The VDR V1 API has also been marked as deprecated. +Nodes running v6 with ``nuts`` configured as one of the ``vdr.did_methods`` will migrate all owned ``did:nuts`` DID documents to the new SQL storage. +This migration includes all historic document updates as published upto a potential deactivation of the document. +For DIDs with a document conflict this is different than the resolved version of the document, which contains a merge of all conflicting document updates. +To prevent the state of the resolver and the SQL storage to be in conflict, all DID document conflicts must be resolved before upgrading to v6. +See ``/status/diagnostics`` if you own any DIDs with a document conflict. If so, use ``/internal/vdr/v1/did/conflicted`` to find the DIDs with a conflict. + +The document migration will run on every restart of the node, meaning that any updates made using the VDR V1 API will be migrated on the next restart. +When switching from the VDR V1 API to the V2 API, the node must be restarted first to migrate any recent changes. diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index af00dec68..c32a39439 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -17,6 +17,7 @@ Breaking changes When migrating from v5, change the owner of the data directory on the host to that of the container's user. (``chown -R 18081:18081 /path/to/host/data-dir``) - Docker image tags have been changed: previously version tags had were prefixed with ``v`` (e.g., ``v5.0.0``), this prefix has been dropped to better adhere to industry standards. - The VDR v1 ``createDID`` (``POST /internal/vdr/v1/did``) no longer supports the ``controller`` and ``selfControl`` fields. All did:nuts documents are now self controlled. All existing documents will be migrated to self controlled at startup. +- Managed ``did:nuts`` DIDs are migrated to the new SQL storage. Unresolved DID document conflicts may contain an incorrect state after migrating to v6. See ``/status/diagnostics`` if you own any DIDs with a document conflict; use ``/internal/vdr/v1/did/conflicted`` to find the specific DIDs. - Removed legacy API authentication tokens. ============ diff --git a/e2e-tests/migration/docker-compose-post-migration.yml b/e2e-tests/migration/docker-compose-post-migration.yml new file mode 100644 index 000000000..37d8d6201 --- /dev/null +++ b/e2e-tests/migration/docker-compose-post-migration.yml @@ -0,0 +1,32 @@ +services: + nodeA: + image: "${IMAGE_NODE_A:-nutsfoundation/nuts-node:master}" + container_name: nodeA + user: &usr "$USER:$USER" + ports: + - "18081:8081" + environment: + NUTS_URL: "http://nodeA:8080" + volumes: + - "./nuts-v6.yaml:/nuts/config/nuts.yaml" + - "./nodeA/data:/nuts/data" + - "../tls-certs/nodeA-certificate.pem:/nuts/config/certificate-and-key.pem:ro" + - "../tls-certs/truststore.pem:/nuts/config/truststore.pem:ro" + healthcheck: + interval: 1s # Make test run quicker by checking health status more often + nodeB: + image: nutsfoundation/nuts-node:v5.4.11 # must be v5.4.11+ for bugfixes. sync with docker-compose-post-migration.yml + container_name: nodeB + user: *usr + ports: + - "28081:1323" + environment: + NUTS_CONFIGFILE: /opt/nuts/nuts.yaml + NUTS_NETWORK_BOOTSTRAPNODES: "nodeA:5555" + volumes: + - "./nuts-v5.yaml:/opt/nuts/nuts.yaml" + - "./nodeB/data:/opt/nuts/data" + - "../tls-certs/nodeB-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" + - "../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" + healthcheck: + interval: 1s # Make test run quicker by checking health status more often \ No newline at end of file diff --git a/e2e-tests/migration/docker-compose-pre-migration.yml b/e2e-tests/migration/docker-compose-pre-migration.yml new file mode 100644 index 000000000..7c5c6f4fc --- /dev/null +++ b/e2e-tests/migration/docker-compose-pre-migration.yml @@ -0,0 +1,32 @@ +services: + nodeA: + image: &im nutsfoundation/nuts-node:v5.4.11 # must be v5.4.11+ for bugfixes. sync with docker-compose-post-migration.yml + container_name: nodeA + user: &usr "$USER:$USER" + ports: # use v6 ports to minimize changes + - "18081:1323" + environment: + NUTS_CONFIGFILE: /opt/nuts/nuts.yaml + volumes: + - "./nuts-v5.yaml:/opt/nuts/nuts.yaml" + - "./nodeA/data:/opt/nuts/data" + - "../tls-certs/nodeA-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" + - "../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" + healthcheck: + interval: 1s # Make test run quicker by checking health status more often + nodeB: + image: *im + container_name: nodeB + user: *usr + ports: + - "28081:1323" + environment: + NUTS_CONFIGFILE: /opt/nuts/nuts.yaml + NUTS_NETWORK_BOOTSTRAPNODES: "nodeA:5555" + volumes: + - "./nuts-v5.yaml:/opt/nuts/nuts.yaml" + - "./nodeB/data:/opt/nuts/data" + - "../tls-certs/nodeB-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" + - "../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" + healthcheck: + interval: 1s # Make test run quicker by checking health status more often \ No newline at end of file diff --git a/e2e-tests/migration/main_test.go b/e2e-tests/migration/main_test.go new file mode 100644 index 000000000..213a34b21 --- /dev/null +++ b/e2e-tests/migration/main_test.go @@ -0,0 +1,120 @@ +//go:build e2e_tests + +/* + * 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 . + * + */ + +package migration + +import ( + "encoding/json" + did "github.com/nuts-foundation/go-did/did" + "github.com/stretchr/testify/assert" + "os" + "testing" + "time" + + "github.com/nuts-foundation/nuts-node/storage" + "github.com/nuts-foundation/nuts-node/storage/orm" + "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/stretchr/testify/require" +) + +func Test_Migrations(t *testing.T) { + db := storage.NewTestStorageEngineInDir(t, "./nodeA/data").GetSQLDatabase() + + DIDs, err := didsubject.NewDIDManager(db).All() + require.NoError(t, err) + require.Len(t, DIDs, 4) + + t.Run("vendor", func(t *testing.T) { + // versions for did:nuts: + // - LC0: init -> no controller because vendor + // - LC4: add service1 + // - LC4: add service2, conflicts with above + // - LC8: add verification method, solves conflict + // no updates during migration + // + // total 4 versions in SQL; latest has 2 services and 2 VMs + id := did.MustParseDID(os.Getenv("VENDOR_DID")) + var doc orm.DidDocument + err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + require.NoError(t, err) + + assert.Equal(t, 3, doc.Version) + assert.Len(t, doc.Services, 2) + assert.Len(t, doc.VerificationMethods, 2) + }) + t.Run("org1", func(t *testing.T) { + // versions for did:nuts: + // - LC1: init -> has controller + // - LC5: add service2 + // - LC6: add service1, conflicts with above + // migration removes controller (solves document conflict) + // + // total 4 versions in SQL; latest one has no controller, 2 services, and 1 VM + id := did.MustParseDID(os.Getenv("ORG1_DID")) + var doc orm.DidDocument + err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + require.NoError(t, err) + + assert.Equal(t, 3, doc.Version) + assert.Len(t, doc.Services, 2) + assert.Len(t, doc.VerificationMethods, 1) + didDoc := new(did.Document) + require.NoError(t, json.Unmarshal([]byte(doc.Raw), didDoc)) + assert.Empty(t, didDoc.Controller) + }) + t.Run("org2", func(t *testing.T) { + // versions for did:nuts: + // - LC2: init -> has controller + // - LC5: deactivate + // - LC6: service2, conflicts with above + // deactivated, so no updates during migration; + // + // total 2 versions in SQL, migration stopped at LC5; no controller, 0 service, 0 VM + id := did.MustParseDID(os.Getenv("ORG2_DID")) + var doc orm.DidDocument + err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + require.NoError(t, err) + + assert.Equal(t, 1, doc.Version) + assert.Len(t, doc.Services, 0) + assert.Len(t, doc.VerificationMethods, 0) + }) + t.Run("org3", func(t *testing.T) { + // versions for did:nuts: + // - LC3: init -> has controller + // - LC7: add service1 + // - LC7: add verification method, conflicts with above + // - LC9: add service2, solves conflict + // migration removes controller, total 5 versions in SQL + // + // total 5 versions in SQL; no controller, 2 services, 2 VMs + id := did.MustParseDID(os.Getenv("ORG3_DID")) + var doc orm.DidDocument + err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + require.NoError(t, err) + + assert.Equal(t, 4, doc.Version) + assert.Len(t, doc.Services, 2) + assert.Len(t, doc.VerificationMethods, 2) + didDoc := new(did.Document) + require.NoError(t, json.Unmarshal([]byte(doc.Raw), didDoc)) + assert.Empty(t, didDoc.Controller) + }) +} diff --git a/e2e-tests/migration/nuts-v5.yaml b/e2e-tests/migration/nuts-v5.yaml new file mode 100644 index 000000000..7cb98a163 --- /dev/null +++ b/e2e-tests/migration/nuts-v5.yaml @@ -0,0 +1,12 @@ +datadir: /opt/nuts/data +verbosity: debug +strictmode: false +tls: + truststorefile: "/opt/nuts/truststore.pem" + certfile: "/opt/nuts/certificate-and-key.pem" + certkeyfile: "/opt/nuts/certificate-and-key.pem" +auth: + contractvalidators: + - dummy +goldenhammer: + enabled: false \ No newline at end of file diff --git a/e2e-tests/migration/nuts-v6.yaml b/e2e-tests/migration/nuts-v6.yaml new file mode 100644 index 000000000..cf8b297bd --- /dev/null +++ b/e2e-tests/migration/nuts-v6.yaml @@ -0,0 +1,15 @@ +verbosity: debug +strictmode: false +#url: http://nodeA:1323 # set in as env variable +tls: + truststorefile: "/nuts/config/truststore.pem" + certfile: "/nuts/config/certificate-and-key.pem" + certkeyfile: "/nuts/config/certificate-and-key.pem" +auth: + contractvalidators: + - dummy +goldenhammer: + enabled: false +http: + internal: + address: :8081 diff --git a/e2e-tests/migration/run-test.sh b/e2e-tests/migration/run-test.sh new file mode 100755 index 000000000..641699ce4 --- /dev/null +++ b/e2e-tests/migration/run-test.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +source ../util.sh +USER=$UID + +######################################################### +# THE ORDER OF TRANSACTIONS IS SIGNIFICANT, DONT CHANGE # +######################################################### + +# createOrg creates a DID under the control of another DID +# Args: controller DID +# Returns: the created DID +function createOrg() { + printf '{ + "selfControl": false, + "controllers": ["%s"], + "assertionMethod": true, + "capabilityInvocation": false + }' "$1" | \ + curl -sS -X POST "http://localhost:18081/internal/vdr/v1/did" -H "Content-Type: application/json" --data-binary @- | jq -r ".id" +} + +# addServiceV1 add a service to a DID document using the vdr/v1 API +# Args: service host, service type, DID to add the service to +# Returns: null +function addServiceV1() { + printf '{ + "type": "%s", + "endpoint": "%s/%s" + }' "$2" "$1" "$2" | \ + curl -sS -X POST "http://localhost:18081/internal/didman/v1/did/$3/endpoint" -H "Content-Type: application/json" --data-binary @- > /dev/null +} + +# addVerificationMethodV1 add a verification method to a DID document using the vdr/v1 API +# Args: DID to add the verification method to +# Returns: null +function addVerificationMethodV1() { + curl -sS -X POST "http://localhost:18081/internal/vdr/v1/did/$1/verificationmethod" > /dev/null +} + +# deactivateDIDV1 deactivates a DID document using the vdr/v1 API +# Args: DID to deactivate +# Returns: null +function deactivateDIDV1() { + curl -sS -X DELETE "http://localhost:18081/internal/vdr/v1/did/$1" > /dev/null +} + +echo "------------------------------------" +echo "Cleaning up running Docker containers and volumes, and key material..." +echo "------------------------------------" +docker compose -f docker-compose-pre-migration.yml down +docker compose -f docker-compose-pre-migration.yml rm -f -v +rm -rf ./node*/ +mkdir -p ./nodeA/{data,backup} ./nodeB/data # 'data' dirs will be created with root owner by docker if they do not exit. This creates permission issues on CI. + +echo "------------------------------------" +echo "Starting Docker containers..." +echo "------------------------------------" +docker compose -f docker-compose-pre-migration.yml up --wait nodeA nodeB + +echo "------------------------------------" +echo "Registering DIDs..." +echo "------------------------------------" +# Register Vendor +VENDOR_DID=$(curl -X POST -sS http://localhost:18081/internal/vdr/v1/did | jq -r .id) +echo Vendor DID: "$VENDOR_DID" +# Register org1 +ORG1_DID=$(createOrg "$VENDOR_DID") +echo Org1 DID: "$ORG1_DID" +# Register org2 +ORG2_DID=$(createOrg "$VENDOR_DID") +echo Org2 DID: "$ORG2_DID" +# Register org3 +ORG3_DID=$(createOrg "$VENDOR_DID") +echo Org3 DID: "$ORG3_DID" + +# Wait for NodeB to contain 4 transactions +waitForTXCount "NodeB" "http://localhost:28081/status/diagnostics" 4 10 + +echo "------------------------------------" +echo "Making backup nodeA..." +echo "------------------------------------" +docker compose -f docker-compose-pre-migration.yml stop nodeA +cp -R ./nodeA/data/* ./nodeA/backup/ +docker compose -f docker-compose-pre-migration.yml up --wait nodeA + +echo "------------------------------------" +echo "Adding and syncing left branch..." +echo "------------------------------------" +addServiceV1 "http://vendor" "service1" "$VENDOR_DID" +deactivateDIDV1 "$ORG2_DID" +addServiceV1 "http://org1" "service1" "$ORG1_DID" +addServiceV1 "http://org3" "service1" "$ORG3_DID" + +# Wait for NodeB to contain 8 transactions +waitForTXCount "NodeB" "http://localhost:28081/status/diagnostics" 8 10 + +echo "------------------------------------" +echo "Restoring backup to nodeA..." +echo "------------------------------------" +docker compose -f docker-compose-pre-migration.yml stop +rm -r ./nodeA/data +mv ./nodeA/backup ./nodeA/data +docker compose -f docker-compose-pre-migration.yml up nodeA --wait nodeA + +echo "------------------------------------" +echo "Adding right branch..." +echo "------------------------------------" +addServiceV1 "http://vendor" "service2" "$VENDOR_DID" +addServiceV1 "http://org1" "service2" "$ORG1_DID" +addServiceV1 "http://org2" "service2" "$ORG2_DID" +addVerificationMethodV1 "$ORG3_DID" + +# Check NodeA contains 8 transactions, nodeB is offline +waitForTXCount "NodeA" "http://localhost:18081/status/diagnostics" 8 10 + +echo "------------------------------------" +echo "Syncing right branch..." +echo "------------------------------------" +docker compose -f docker-compose-pre-migration.yml up --wait nodeB +# sync left and right branch through nodeB to create document conflicts on all DIDs + +# Wait for NodeB to contain 12 transactions +waitForTXCount "NodeB" "http://localhost:28081/status/diagnostics" 12 10 + +echo "------------------------------------" +echo "Fix some DID document conflicts..." +echo "------------------------------------" +addVerificationMethodV1 "$VENDOR_DID" +addServiceV1 "http://org3" "service2" "$ORG3_DID" +# ORG1 is in conflicted state +# ORG2 is conflicted but deactivated + +# Wait for NodeB to contain 14 transactions +waitForTXCount "NodeB" "http://localhost:28081/status/diagnostics" 14 10 + +echo "------------------------------------" +echo "Upgrade nodeA to v6..." +echo "------------------------------------" +docker compose -f docker-compose-pre-migration.yml down +docker compose -f docker-compose-post-migration.yml up --wait nodeA nodeB +# controller migration: +2 transactions: remove controllers from ORG1 and ORG3 + +# Wait for NodeB to contain 16 transactions +waitForTXCount "NodeB" "http://localhost:28081/status/diagnostics" 16 10 + +echo "------------------------------------" +echo "Stopping Docker containers..." +echo "------------------------------------" +docker compose -f docker-compose-post-migration.yml stop + +echo "------------------------------------" +echo "Verifying migration results..." +echo "------------------------------------" +# all 'waitForTXCount' calls have confirmed the didstore is (likely to be) in the correct state. Now check SQL store. + +VENDOR_DID=$VENDOR_DID ORG1_DID=$ORG1_DID ORG2_DID=$ORG2_DID ORG3_DID=$ORG3_DID go test -v --tags=e2e_tests -count=1 . +if [ $? -ne 0 ]; then + echo "ERROR: test failure" + exitWithDockerLogs 1 docker-compose-post-migration.yml +fi \ No newline at end of file diff --git a/e2e-tests/migration/run-tests.sh b/e2e-tests/migration/run-tests.sh new file mode 100755 index 000000000..e0f9acf11 --- /dev/null +++ b/e2e-tests/migration/run-tests.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e # make script fail if any of the tests returns a non-zero exit code + +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "!! Running test: Migrations v6 !!" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +./run-test.sh \ No newline at end of file diff --git a/e2e-tests/run-tests.sh b/e2e-tests/run-tests.sh index c6df3fb2a..98146cf5b 100755 --- a/e2e-tests/run-tests.sh +++ b/e2e-tests/run-tests.sh @@ -57,3 +57,10 @@ echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" pushd discovery ./run-tests.sh popd + +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "!! Running test suite: Migration !!" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +pushd migration +./run-tests.sh +popd diff --git a/e2e-tests/util.sh b/e2e-tests/util.sh index e1306da1b..b140fb94c 100644 --- a/e2e-tests/util.sh +++ b/e2e-tests/util.sh @@ -57,10 +57,13 @@ function waitForDiagnostic { echo "" } +# exitWithDockerLogs prints the logs of the containers and exits with a specified exit code +# Args: exit code; docker compose file to use for the logs (default: docker-compose.yml) function exitWithDockerLogs { EXIT_CODE=$1 - docker compose logs - docker compose down + COMPOSE_FILE="${2:-docker-compose.yml}" + docker compose -f $COMPOSE_FILE logs + docker compose -f $COMPOSE_FILE stop exit $EXIT_CODE } diff --git a/storage/orm/did_document.go b/storage/orm/did_document.go index f0d6a05a2..d0def0273 100644 --- a/storage/orm/did_document.go +++ b/storage/orm/did_document.go @@ -20,6 +20,9 @@ package orm import ( "encoding/json" + "time" + + "github.com/google/uuid" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/jsonld" "gorm.io/gorm/schema" @@ -102,3 +105,65 @@ func (sqlDoc DidDocument) GenerateDIDDocument() (did.Document, error) { return document, nil } + +// MigrationDocument is used to convert a did.Document + metadata to a DidDocument. +// DEPRECATED: only intended to migrate owned did:nuts to SQL storage. +type MigrationDocument struct { + // Raw contains the did.Document in bytes. For did:nuts this is must be equal to the payload in the network transaction. + Raw []byte + Created time.Time + Updated time.Time + Version int +} + +// ToORMDocument converts the Raw document to a DidDocument. Generates a new DidDocument.ID +func (migration MigrationDocument) ToORMDocument(subject string) (DidDocument, error) { + doc := new(did.Document) + err := json.Unmarshal(migration.Raw, doc) + if err != nil { + return DidDocument{}, err + } + + // generate DB documentID + documentID := uuid.New().String() + + // convert []did.VerificationMethod to []VerificationMethod + vms := make([]VerificationMethod, len(doc.VerificationMethod)) + for i, vm := range doc.VerificationMethod { + vmAsJson, _ := json.Marshal(*vm) + vms[i] = VerificationMethod{ + ID: vm.ID.String(), + KeyTypes: VerificationMethodKeyType(verificationMethodToKeyFlags(*doc, vm)), + Data: vmAsJson, + } + } + + // convert []did.Service to []Service + services := make([]Service, len(doc.Service)) + for i, service := range doc.Service { + asJson, _ := json.Marshal(service) + services[i] = Service{ + ID: service.ID.String(), + Data: asJson, + } + } + + // DID + subjectDID := DID{ + ID: doc.ID.String(), + Subject: subject, + } + + // return document + return DidDocument{ + ID: documentID, + DidID: doc.ID.String(), + DID: subjectDID, + CreatedAt: migration.Created.Unix(), + UpdatedAt: migration.Updated.Unix(), + Version: migration.Version, + VerificationMethods: vms, + Services: services, + Raw: string(migration.Raw), + }, nil +} diff --git a/storage/orm/did_document_test.go b/storage/orm/did_document_test.go index 8a532ded0..9c2f88636 100644 --- a/storage/orm/did_document_test.go +++ b/storage/orm/did_document_test.go @@ -1,11 +1,12 @@ package orm import ( + "encoding/json" "github.com/nuts-foundation/go-did/did" - "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "testing" + "time" ) var ( @@ -44,3 +45,65 @@ func TestDIDDocument_ToDIDDocument(t *testing.T) { assert.Equal(t, "#1", didDoc.VerificationMethod[0].ID.String()) assert.Equal(t, "#2", didDoc.Service[0].ID.String()) } + +func TestDIDDocument_FromDIDDocument(t *testing.T) { + created := time.Now() + updated := created.Add(time.Second) + version := 4 + vms := []VerificationMethod{ + { + ID: "#1", + Data: []byte(`{"id":"#1"}`), + KeyTypes: VerificationMethodKeyType(AssertionMethodUsage | KeyAgreementUsage), + }, { + ID: "#2", + Data: []byte(`{"id":"#2"}`), + KeyTypes: VerificationMethodKeyType(KeyAgreementUsage), + }, + } + service := Service{ + ID: "#service", + Data: []byte(`{"id":"#service"}`), + } + didDoc, err := DidDocument{ + DID: DID{ID: alice.String()}, + VerificationMethods: vms, + Services: []Service{service}, + CreatedAt: created.Unix(), + UpdatedAt: updated.Unix(), + }.ToDIDDocument() + require.NoError(t, err) + docRaw, err := json.Marshal(didDoc) + require.NoError(t, err) + + result, err := MigrationDocument{ + Version: version, + Created: created, + Updated: updated, + Raw: docRaw, + }.ToORMDocument("test-subject") + require.NoError(t, err) + + assert.NotEmpty(t, result.ID) + assert.Equal(t, alice.String(), result.DidID) + assert.Equal(t, created.Unix(), result.CreatedAt) + assert.Equal(t, updated.Unix(), result.UpdatedAt) + assert.Equal(t, version, result.Version) + + // DID + assert.Equal(t, DID{ + ID: alice.String(), + Subject: "test-subject", + }, result.DID) + + // Services + require.Len(t, result.Services, 1) + assert.Equal(t, service, result.Services[0]) + + // VerificationMethods + require.Len(t, result.VerificationMethods, 2) + assert.Equal(t, "#1", result.VerificationMethods[0].ID) + assert.Equal(t, VerificationMethodKeyType(AssertionMethodUsage|KeyAgreementUsage), result.VerificationMethods[0].KeyTypes) + assert.Equal(t, "#2", result.VerificationMethods[1].ID) + assert.Equal(t, VerificationMethodKeyType(KeyAgreementUsage), result.VerificationMethods[1].KeyTypes) +} diff --git a/storage/orm/keyflag.go b/storage/orm/keyflag.go index 46a13ff8d..b04f8dc43 100644 --- a/storage/orm/keyflag.go +++ b/storage/orm/keyflag.go @@ -18,6 +18,8 @@ package orm +import "github.com/nuts-foundation/go-did/did" + // DIDKeyFlags is a bitmask used for specifying for what purposes a key in a DID document can be used (a.k.a. Verification Method relationships). type DIDKeyFlags uint @@ -46,3 +48,24 @@ func AssertionKeyUsage() DIDKeyFlags { func EncryptionKeyUsage() DIDKeyFlags { return KeyAgreementUsage } + +// verificationMethodToKeyFlags creates DIDKeyFlags for a did.VerificationMethod based on its usage in the did.Document. +func verificationMethodToKeyFlags(document did.Document, vm *did.VerificationMethod) DIDKeyFlags { + var flags DIDKeyFlags + if document.Authentication.FindByID(vm.ID) != nil { + flags |= AuthenticationUsage + } + if document.AssertionMethod.FindByID(vm.ID) != nil { + flags |= AssertionMethodUsage + } + if document.CapabilityDelegation.FindByID(vm.ID) != nil { + flags |= CapabilityDelegationUsage + } + if document.CapabilityInvocation.FindByID(vm.ID) != nil { + flags |= CapabilityInvocationUsage + } + if document.KeyAgreement.FindByID(vm.ID) != nil { + flags |= KeyAgreementUsage + } + return flags +} diff --git a/storage/orm/keyflag_test.go b/storage/orm/keyflag_test.go index db6fbe57e..550c6e180 100644 --- a/storage/orm/keyflag_test.go +++ b/storage/orm/keyflag_test.go @@ -19,6 +19,7 @@ package orm import ( + "github.com/nuts-foundation/go-did/did" "testing" "github.com/stretchr/testify/assert" @@ -55,3 +56,43 @@ func TestKeyUsage_Is(t *testing.T) { } }) } + +func Test_verificationMethodToKeyFlags(t *testing.T) { + vr1 := did.VerificationRelationship{VerificationMethod: &did.VerificationMethod{ + ID: did.MustParseDIDURL("did:method:something#key-1"), + }} + vr2 := did.VerificationRelationship{VerificationMethod: &did.VerificationMethod{ + ID: did.MustParseDIDURL("did:method:something#key-2"), + }} + t.Run("single-key", func(t *testing.T) { + t.Run("AssertionMethod", func(t *testing.T) { + doc := did.Document{AssertionMethod: did.VerificationRelationships{vr1}} + assert.Equal(t, AssertionMethodUsage, verificationMethodToKeyFlags(doc, vr1.VerificationMethod)) + }) + t.Run("Authentication", func(t *testing.T) { + doc := did.Document{Authentication: did.VerificationRelationships{vr1}} + assert.Equal(t, AuthenticationUsage, verificationMethodToKeyFlags(doc, vr1.VerificationMethod)) + }) + t.Run("CapabilityDelegation", func(t *testing.T) { + doc := did.Document{CapabilityDelegation: did.VerificationRelationships{vr1}} + assert.Equal(t, CapabilityDelegationUsage, verificationMethodToKeyFlags(doc, vr1.VerificationMethod)) + }) + t.Run("CapabilityInvocation", func(t *testing.T) { + doc := did.Document{CapabilityInvocation: did.VerificationRelationships{vr1}} + assert.Equal(t, CapabilityInvocationUsage, verificationMethodToKeyFlags(doc, vr1.VerificationMethod)) + }) + t.Run("KeyAgreement", func(t *testing.T) { + doc := did.Document{KeyAgreement: did.VerificationRelationships{vr1}} + assert.Equal(t, KeyAgreementUsage, verificationMethodToKeyFlags(doc, vr1.VerificationMethod)) + }) + }) + t.Run("multi-key", func(t *testing.T) { + doc := did.Document{ + AssertionMethod: did.VerificationRelationships{vr1}, + CapabilityInvocation: did.VerificationRelationships{vr1, vr2}, + KeyAgreement: did.VerificationRelationships{vr2}, + } + assert.Equal(t, AssertionMethodUsage|CapabilityInvocationUsage, verificationMethodToKeyFlags(doc, vr1.VerificationMethod)) + assert.Equal(t, CapabilityInvocationUsage|KeyAgreementUsage, verificationMethodToKeyFlags(doc, vr2.VerificationMethod)) + }) +} diff --git a/storage/test.go b/storage/test.go index 21b348dd6..e6f0b432b 100644 --- a/storage/test.go +++ b/storage/test.go @@ -21,6 +21,7 @@ package storage import ( "context" "errors" + "fmt" "github.com/alicebob/miniredis/v2" "github.com/nuts-foundation/go-stoabs" "github.com/nuts-foundation/nuts-node/test/io" @@ -70,6 +71,7 @@ func NewTestStorageEngineInDir(t testing.TB, dir string) Engine { t.Cleanup(func() { _ = result.Shutdown() }) + fmt.Printf("Created test storage engine in %s\n", dir) return result } diff --git a/vdr/didnuts/didstore/interface.go b/vdr/didnuts/didstore/interface.go index 22ee14cc6..d180c7741 100644 --- a/vdr/didnuts/didstore/interface.go +++ b/vdr/didnuts/didstore/interface.go @@ -20,6 +20,7 @@ package didstore import ( "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/nuts-node/storage/orm" "github.com/nuts-foundation/nuts-node/vdr/resolver" ) @@ -42,6 +43,11 @@ type Store interface { // It returns vdr.ErrNotFound if there are no corresponding DID documents or when the DID Documents are disjoint with the provided ResolveMetadata. // It returns vdr.ErrDeactivated if no metadata is given and the latest version of the DID Document is deactivated. Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) + // HistorySinceVersion returns all versions of the DID Document since the specified version (including version). + // The slice is empty when version is the most recent version of the DID Document. + // The history contains DID Documents as they were published, which differs from Resolve that produces a merge of conflicted documents. + // DEPRECATED: This function exists to migrate the history of owned DIDs from key-value storage to SQL storage. + HistorySinceVersion(id did.DID, version int) ([]orm.MigrationDocument, error) } // Transaction is an alias to the didstore.event. Internally to the didstore it's an event based on a transaction. diff --git a/vdr/didnuts/didstore/mock.go b/vdr/didnuts/didstore/mock.go index 9991b42f1..febc56696 100644 --- a/vdr/didnuts/didstore/mock.go +++ b/vdr/didnuts/didstore/mock.go @@ -13,6 +13,7 @@ import ( reflect "reflect" did "github.com/nuts-foundation/go-did/did" + orm "github.com/nuts-foundation/nuts-node/storage/orm" resolver "github.com/nuts-foundation/nuts-node/vdr/resolver" gomock "go.uber.org/mock/gomock" ) @@ -98,6 +99,21 @@ func (mr *MockStoreMockRecorder) DocumentCount() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DocumentCount", reflect.TypeOf((*MockStore)(nil).DocumentCount)) } +// HistorySinceVersion mocks base method. +func (m *MockStore) HistorySinceVersion(id did.DID, version int) ([]orm.MigrationDocument, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HistorySinceVersion", id, version) + ret0, _ := ret[0].([]orm.MigrationDocument) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HistorySinceVersion indicates an expected call of HistorySinceVersion. +func (mr *MockStoreMockRecorder) HistorySinceVersion(id, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HistorySinceVersion", reflect.TypeOf((*MockStore)(nil).HistorySinceVersion), id, version) +} + // Iterate mocks base method. func (m *MockStore) Iterate(fn resolver.DocIterator) error { m.ctrl.T.Helper() diff --git a/vdr/didnuts/didstore/store.go b/vdr/didnuts/didstore/store.go index 5b2d44c13..389062e91 100644 --- a/vdr/didnuts/didstore/store.go +++ b/vdr/didnuts/didstore/store.go @@ -26,7 +26,10 @@ import ( "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-stoabs" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto/hash" "github.com/nuts-foundation/nuts-node/storage" + "github.com/nuts-foundation/nuts-node/storage/orm" + "github.com/nuts-foundation/nuts-node/vdr/log" "github.com/nuts-foundation/nuts-node/vdr/resolver" ) @@ -349,3 +352,60 @@ func latestNonDeactivatedRequested(resolveMetadata *resolver.ResolveMetadata) bo } return !resolveMetadata.AllowDeactivated } + +func (tl *store) HistorySinceVersion(id did.DID, version int) ([]orm.MigrationDocument, error) { + if version < 0 { + return nil, errors.New("negative version") + } + var history []orm.MigrationDocument + txErr := tl.db.Read(context.TODO(), func(tx stoabs.ReadTx) error { + el, err := readEventList(tx, id) + if err != nil { + return err + } + if len(el.Events) == 0 { + return storage.ErrNotFound + } + highestDIDStoreVersion := len(el.Events) - 1 + + // return if no changes + if version > highestDIDStoreVersion { + return nil + } + + created := el.Events[0].SigningTime + documentReader := tx.GetShelfReader(documentShelf) + history = make([]orm.MigrationDocument, 0, highestDIDStoreVersion-version+1) + for v := version; v <= highestDIDStoreVersion; v++ { + payloadHash := el.Events[v].PayloadHash + documentBytes, err := documentReader.Get(stoabs.NewHashKey(payloadHash)) + if err != nil { + if errors.Is(err, stoabs.ErrKeyNotFound) { + return storage.ErrNotFound + } + return err + } + // We expect documentBytes to be equal to the raw payload on the DAG because we are the publisher of the document and use the same un/marshal methods everywhere. + // This will break if did.Document in go-did is changed. + // Confirmed this is safe/true for all documents on prd network as of 13-09-24, so for now just log if they are not the same. + if !payloadHash.Equals(hash.SHA256Sum(documentBytes)) { + log.Logger(). + WithField("payload hash", payloadHash). + WithField("document hash", hash.SHA256Sum(documentBytes)). + WithField("document version", version). + Error("Payload hash does not match hash of DID Document during did:nuts history migration") + } + history = append(history, orm.MigrationDocument{ + Raw: documentBytes, + Created: created, + Updated: el.Events[v].SigningTime, + Version: v, + }) + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return history, nil +} diff --git a/vdr/didnuts/didstore/store_test.go b/vdr/didnuts/didstore/store_test.go index d3d42503e..bcf0f726f 100644 --- a/vdr/didnuts/didstore/store_test.go +++ b/vdr/didnuts/didstore/store_test.go @@ -29,6 +29,7 @@ import ( "github.com/nuts-foundation/go-stoabs" "github.com/nuts-foundation/go-stoabs/redis7" "github.com/nuts-foundation/nuts-node/crypto/hash" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" @@ -454,3 +455,77 @@ func Test_matches(t *testing.T) { }) }) } + +func TestStore_HistorySinceVersion(t *testing.T) { + store := NewTestStore(t) + + // create DID document with some updates and a document conflict + doc1 := did.Document{ID: testDID, Controller: []did.DID{testDID}, Service: []did.Service{testServiceA}} + tx1 := newTestTransaction(doc1) // serviceA + tx1.SigningTime = time.Now().Add(-time.Second) + doc2a := did.Document{ID: testDID, Service: []did.Service{testServiceA}} + tx2a := newTestTransaction(doc2a, tx1.Ref) // deactivate + doc2b := did.Document{ID: testDID, Controller: []did.DID{testDID}, Service: []did.Service{testServiceB}} + tx2b := newTestTransaction(doc2b, tx1.Ref) // serviceB + doc3 := did.Document{ID: testDID, Controller: []did.DID{testDID}, Service: []did.Service{testServiceA, testServiceB}} + tx3 := newTestTransaction(doc3, tx2a.Ref, tx2b.Ref) // serviceA + serviceB + + // add all transactions. + // txs all have LC=0, so they are sorted on SigningTime. Nano-sec timestamps guarantee that the tx{1,2a,2b,3} order is preserved + require.NoError(t, store.Add(doc1, tx1)) + require.NoError(t, store.Add(doc2a, tx2a)) + require.NoError(t, store.Add(doc2b, tx2b)) + require.NoError(t, store.Add(doc3, tx3)) + + // raw documents in order + raw := [4][]byte{} + raw[0], _ = json.Marshal(doc1) + raw[1], _ = json.Marshal(doc2a) + raw[2], _ = json.Marshal(doc2b) + raw[3], _ = json.Marshal(doc3) + + t.Run("ok - full history", func(t *testing.T) { + history, err := store.HistorySinceVersion(testDID, 0) + assert.NoError(t, err) + require.Len(t, history, 4) + + for idx, tx := range []Transaction{tx1, tx2a, tx2b, tx3} { + result := history[idx] + assert.Equal(t, raw[idx], result.Raw) // make sure result.Raw contains the original documents, not the merged document conflicts + assert.True(t, tx1.SigningTime.Equal(result.Created)) + assert.True(t, tx.SigningTime.Equal(result.Updated)) + assert.Equal(t, idx, result.Version) + } + }) + t.Run("ok - partial history", func(t *testing.T) { + history, err := store.HistorySinceVersion(testDID, 1) + assert.NoError(t, err) + require.Len(t, history, 3) + assert.Equal(t, 1, history[0].Version) + }) + t.Run("ok - no version updates", func(t *testing.T) { + history, err := store.HistorySinceVersion(testDID, 5) + assert.NoError(t, err) + assert.Len(t, history, 0) + }) + t.Run("error - negative version", func(t *testing.T) { + history, err := store.HistorySinceVersion(testDID, -1) + assert.EqualError(t, err, "negative version") + assert.Nil(t, history) + }) + t.Run("error - unknown DID", func(t *testing.T) { + history, err := store.HistorySinceVersion(did.MustParseDID("did:nuts:unknown"), 0) + assert.ErrorIs(t, err, storage.ErrNotFound) + assert.Nil(t, history) + }) + t.Run("error - document version not found", func(t *testing.T) { + err := store.db.WriteShelf(context.Background(), documentShelf, func(writer stoabs.Writer) error { + return writer.Delete(stoabs.NewHashKey(tx1.PayloadHash)) + }) + require.NoError(t, err) + + history, err := store.HistorySinceVersion(testDID, 0) + assert.ErrorIs(t, err, storage.ErrNotFound) + assert.Nil(t, history) + }) +} diff --git a/vdr/didsubject/did_document.go b/vdr/didsubject/did_document.go index f8b283c2c..3a1579756 100644 --- a/vdr/didsubject/did_document.go +++ b/vdr/didsubject/did_document.go @@ -88,5 +88,5 @@ func (s *SqlDIDDocumentManager) Latest(did did.DID, resolveTime *time.Time) (*or if err != nil { return nil, err } - return &doc, err + return &doc, nil } diff --git a/vdr/didsubject/interface.go b/vdr/didsubject/interface.go index 0c7d17c91..d718a9bb6 100644 --- a/vdr/didsubject/interface.go +++ b/vdr/didsubject/interface.go @@ -135,6 +135,14 @@ type Manager interface { Rollback(ctx context.Context) } +// DocumentMigration is used to migrate DID document versions to the SQL DB. This should only be used for DID documents managed by this node. +type DocumentMigration interface { + // MigrateDIDHistoryToSQL is used to migrate the history of a DID Document to SQL. + // It adds all versions of a DID Document up to a deactivated version. Any changes after a deactivation are not migrated. + // getHistory retrieves the history of the DID since the requested version. + MigrateDIDHistoryToSQL(id did.DID, subject string, getHistory func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error)) error +} + // SubjectCreationOption links all create DIDs to the DID Subject type SubjectCreationOption struct { Subject string diff --git a/vdr/didsubject/manager.go b/vdr/didsubject/manager.go index 1888ac952..334780e9b 100644 --- a/vdr/didsubject/manager.go +++ b/vdr/didsubject/manager.go @@ -33,6 +33,7 @@ import ( "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/storage/orm" "github.com/nuts-foundation/nuts-node/vdr/log" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "gorm.io/gorm" "regexp" "sort" @@ -134,8 +135,7 @@ func (r *SqlManager) Create(ctx context.Context, options CreationOptions) ([]did sqlDocs := make(map[string]orm.DidDocument) err := r.transactionHelper(ctx, func(tx *gorm.DB) (map[string]orm.DIDChangeLog, error) { // check existence - sqlDIDManager := NewDIDManager(tx) - _, err := sqlDIDManager.FindBySubject(subject) + _, err := NewDIDManager(tx).FindBySubject(subject) if errors.Is(err, ErrSubjectNotFound) { // this is ok, doesn't exist yet } else if err != nil { @@ -648,3 +648,76 @@ func sortDIDDocumentsByMethod(list []did.Document, methodOrder []string) { } copy(list, orderedList) } + +func (r *SqlManager) MigrateDIDHistoryToSQL(id did.DID, subject string, getHistory func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error)) error { + latestSQLVersion := -1 // -1 means it's a new DID + latestORMDocument, err := NewDIDDocumentManager(r.DB).Latest(id, nil) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + // new DID, don't update latestSQLVersion + } else { + // don't migrate DID documents past their deactivation + latestDIDDocument, err := latestORMDocument.ToDIDDocument() + if err != nil { + return err + } + if resolver.IsDeactivated(latestDIDDocument) { + return nil + } + // set latestSQLVersion + latestSQLVersion = latestORMDocument.Version + } + + // get all new document updates + // NOTE: this assumes updates are only appended to the end. This breaks if the history of a document is altered. + history, err := getHistory(id, latestSQLVersion+1) + if err != nil { + return err + } + if len(history) == 0 { + return nil + } + + // convert history to orm objects + documentVersions := make([]orm.DidDocument, len(history)) + for i, entry := range history { + documentVersions[i], err = entry.ToORMDocument(subject) + if err != nil { + return err + } + + // break if this version deactivates the document + didDocument, err := documentVersions[i].ToDIDDocument() + if err != nil { + return err + } + if resolver.IsDeactivated(didDocument) { + documentVersions = documentVersions[:i+1] + break + } + } + + return r.DB.Transaction(func(tx *gorm.DB) error { + if latestSQLVersion == -1 { + // add subject to did table + DID := orm.DID{ + ID: id.String(), + Subject: subject, + } + err = tx.Create(&DID).Error + if err != nil { + return err + } + } + // add document history + for _, doc := range documentVersions { + err = tx.Create(&doc).Error + if err != nil { + return err + } + } + return nil + }) +} diff --git a/vdr/didsubject/manager_test.go b/vdr/didsubject/manager_test.go index a46b4d8e9..9134b71e6 100644 --- a/vdr/didsubject/manager_test.go +++ b/vdr/didsubject/manager_test.go @@ -20,8 +20,11 @@ package didsubject import ( "context" + "encoding/json" + "errors" "fmt" ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/nuts-node/crypto/storage/spi" "github.com/nuts-foundation/nuts-node/storage/orm" "net/url" "strings" @@ -482,3 +485,112 @@ func Test_sortDIDDocumentsByMethod(t *testing.T) { assert.Equal(t, "did:test:1", documents[0].ID.String()) assert.Equal(t, "did:example:1", documents[1].ID.String()) } + +func TestSqlManager_MigrateDIDHistoryToSQL(t *testing.T) { + testDID := did.MustParseDID("did:nuts:test") + vmID := did.MustParseDIDURL("did:nuts:test#key-1") + key, _ := spi.GenerateKeyPair() + vm, err := did.NewVerificationMethod(vmID, ssi.JsonWebKey2020, testDID, key.Public()) + require.NoError(t, err) + service := did.Service{ + ID: ssi.MustParseURI(testDID.String() + "#service-1"), + Type: "test", + ServiceEndpoint: "https://example.com", + } + created := time.Now().Add(-5 * time.Second) + subject := "test-subject" + + rawDocNew, err := json.Marshal(did.Document{ID: testDID, CapabilityInvocation: []did.VerificationRelationship{{VerificationMethod: vm}}, VerificationMethod: did.VerificationMethods{vm}}) + require.NoError(t, err) + rawDocUpdate, _ := json.Marshal(did.Document{ID: testDID, Service: []did.Service{service}, CapabilityInvocation: []did.VerificationRelationship{{VerificationMethod: vm}}}) + rawDocDeactivate, _ := json.Marshal(did.Document{ID: testDID}) + ormMigrateNew := orm.MigrationDocument{Raw: rawDocNew, Created: created, Updated: created, Version: 0} + ormMigrateUpdate := orm.MigrationDocument{Raw: rawDocUpdate, Created: created, Updated: created.Add(2 * time.Second), Version: 1} + ormMigrateDeactivate := orm.MigrationDocument{Raw: rawDocDeactivate, Created: created, Updated: created.Add(2 * time.Second), Version: 1} + ormDocNew, err := ormMigrateNew.ToORMDocument(subject) + ormDocUpdate, _ := ormMigrateUpdate.ToORMDocument(subject) + ormDocDeactivate, _ := ormMigrateDeactivate.ToORMDocument(subject) + equal := func(t *testing.T, o1, o2 orm.DidDocument) { + //assert.NotEqual(t, o1.ID, o2.ID) // ToORMDocument generates a random ID everytime. Should probably be ignored. + assert.Equal(t, o1.DidID, o2.DidID) + assert.Equal(t, o1.DID, o2.DID) + assert.Equal(t, o1.CreatedAt, o2.CreatedAt) + assert.Equal(t, o1.UpdatedAt, o2.UpdatedAt) + assert.Equal(t, o1.Version, o2.Version) + assert.Equal(t, o1.VerificationMethods, o2.VerificationMethods) + assert.Equal(t, o1.Services, o2.Services) + assert.Equal(t, o1.Raw, o2.Raw) + } + t.Run("create", func(t *testing.T) { + db := storage.NewTestStorageEngine(t).GetSQLDatabase() + manager := SqlManager{DB: db} + + err = manager.MigrateDIDHistoryToSQL(testDID, subject, func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error) { + assert.Equal(t, testDID, id) + assert.Equal(t, 0, sinceVersion) + return []orm.MigrationDocument{ormMigrateNew, ormMigrateUpdate}, nil + }) + require.NoError(t, err) + + // version 0 + ormDoc, err := NewDIDDocumentManager(db).Latest(testDID, &created) + require.NoError(t, err) + equal(t, *ormDoc, ormDocNew) + // version 1 + ormDoc, err = NewDIDDocumentManager(db).Latest(testDID, nil) + require.NoError(t, err) + equal(t, *ormDoc, ormDocUpdate) + }) + t.Run("update", func(t *testing.T) { + db := storage.NewTestStorageEngine(t).GetSQLDatabase() + manager := SqlManager{DB: db} + + // init version 0 + err = manager.MigrateDIDHistoryToSQL(testDID, subject, func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error) { + assert.Equal(t, 0, sinceVersion) + return []orm.MigrationDocument{ormMigrateNew}, nil + }) + require.NoError(t, err) + ormDoc, err := NewDIDDocumentManager(db).Latest(testDID, nil) + require.NoError(t, err) + equal(t, *ormDoc, ormDocNew) + + // update to version 1 + err = manager.MigrateDIDHistoryToSQL(testDID, subject, func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error) { + assert.Equal(t, 1, sinceVersion) + return []orm.MigrationDocument{ormMigrateUpdate}, nil + }) + require.NoError(t, err) + ormDoc, err = NewDIDDocumentManager(db).Latest(testDID, nil) + require.NoError(t, err) + equal(t, *ormDoc, ormDocUpdate) + }) + t.Run("deactivate", func(t *testing.T) { + db := storage.NewTestStorageEngine(t).GetSQLDatabase() + manager := SqlManager{DB: db} + + // create in version 0, deactivate in version 1 + err = manager.MigrateDIDHistoryToSQL(testDID, subject, func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error) { + return []orm.MigrationDocument{ormMigrateNew, ormMigrateDeactivate}, nil + }) + require.NoError(t, err) + ormDoc, err := NewDIDDocumentManager(db).Latest(testDID, nil) + require.NoError(t, err) + equal(t, *ormDoc, ormDocDeactivate) + + // don't update deactivated document + assert.NotPanics(t, func() { + _ = manager.MigrateDIDHistoryToSQL(testDID, subject, func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error) { + panic("function should not be called") + }) + }) + }) + t.Run("error - getHistory", func(t *testing.T) { + db := storage.NewTestStorageEngine(t).GetSQLDatabase() + manager := SqlManager{DB: db} + err = manager.MigrateDIDHistoryToSQL(testDID, subject, func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error) { + return nil, errors.New("test") + }) + assert.EqualError(t, err, "test") + }) +} diff --git a/vdr/vdr.go b/vdr/vdr.go index ce61044dd..dcd4234b5 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -77,6 +77,8 @@ type Module struct { keyStore crypto.KeyStore storageInstance storage.Engine eventManager events.Event + // migrations are registered functions to simplify testing + migrations map[string]migration // new style DID management didsubject.Manager @@ -124,6 +126,7 @@ func NewVDR(cryptoClient crypto.KeyStore, networkClient network.Transactions, } m.ctx, m.cancel = context.WithCancel(context.Background()) m.routines = new(sync.WaitGroup) + m.migrations = m.allMigrations() return m } @@ -164,17 +167,19 @@ func (r *Module) Configure(config core.ServerConfig) error { // Register DID resolver and DID methods we can resolve r.ownedDIDResolver = didsubject.Resolver{DB: db} - // Methods we can produce from the Nuts node - // did:nuts - nutsManager := didnuts.NewManager(r.keyStore, r.network, r.store, r.didResolver, db) - r.nutsDocumentManager = nutsManager - methodManagers = map[string]didsubject.MethodManager{} r.documentOwner = &MultiDocumentOwner{ DocumentOwners: []didsubject.DocumentOwner{ newCachingDocumentOwner(DBDocumentOwner{DB: db}, r.didResolver), newCachingDocumentOwner(privateKeyDocumentOwner{keyResolver: r.keyStore}, r.didResolver), }, } + + // Methods we can produce from the Nuts node + methodManagers = map[string]didsubject.MethodManager{} + + // did:nuts + nutsManager := didnuts.NewManager(r.keyStore, r.network, r.store, r.didResolver, db) + r.nutsDocumentManager = nutsManager if slices.Contains(r.config.DIDMethods, didnuts.MethodName) { methodManagers[didnuts.MethodName] = nutsManager r.didResolver.(*resolver.DIDResolverRouter).Register(didnuts.MethodName, &didnuts.Resolver{Store: r.store}) @@ -361,40 +366,82 @@ func (r *Module) Migrate() error { if err != nil { return err } - auditContext := audit.Context(context.Background(), "system", ModuleName, "migrate") + + // only migrate if did:nuts is activated on the node + if slices.Contains(r.SupportedMethods(), "nuts") { + for name, migrate := range r.migrations { + log.Logger().Infof("Running did:nuts migration: '%s'", name) + migrate(owned) + } + } + return nil +} + +func (r *Module) allMigrations() map[string]migration { + return map[string]migration{ // key will be printed as description of the migration + "remove controller": r.migrateRemoveControllerFromDIDNuts, // must come before migrateHistoryOwnedDIDNuts so controller removal is also migrated. + "document history": r.migrateHistoryOwnedDIDNuts, + } +} + +// migrateRemoveControllerFromDIDNuts removes the controller from all did:nuts identifiers under own control. +// This ignores any DIDs that are not did:nuts. +func (r *Module) migrateRemoveControllerFromDIDNuts(owned []did.DID) { + auditContext := audit.Context(context.Background(), "system", ModuleName, "migrate_remove_did_nuts_controller") // resolve the DID Document if the did starts with did:nuts for _, did := range owned { - if did.Method == didnuts.MethodName { - doc, _, err := r.Resolve(did, nil) - if err != nil { - if !(errors.Is(err, resolver.ErrDeactivated) || errors.Is(err, resolver.ErrNoActiveController)) { - log.Logger().WithError(err).WithField(core.LogFieldDID, did.String()).Error("Could not update owned DID document, continuing with next document") - } - continue + if did.Method != didnuts.MethodName { // skip non did:nuts + continue + } + doc, _, err := r.Resolve(did, nil) + if err != nil { + if !(errors.Is(err, resolver.ErrDeactivated) || errors.Is(err, resolver.ErrNoActiveController)) { + log.Logger().WithError(err).WithField(core.LogFieldDID, did.String()).Error("Could not update owned DID document, continuing with next document") } - if len(doc.Controller) > 0 { - doc.Controller = nil - - if len(doc.VerificationMethod) == 0 { - log.Logger().WithField(core.LogFieldDID, doc.ID.String()).Warnf("No verification method found in owned DID document") - continue - } - - if len(doc.CapabilityInvocation) == 0 { - // add all keys as capabilityInvocation keys - for _, vm := range doc.VerificationMethod { - doc.CapabilityInvocation.Add(vm) - } - } - - err = r.nutsDocumentManager.Update(auditContext, did, *doc) - if err != nil { - if !(errors.Is(err, resolver.ErrKeyNotFound)) { - log.Logger().WithError(err).WithField(core.LogFieldDID, did.String()).Error("Could not update owned DID document, continuing with next document") - } - } + continue + } + if len(doc.Controller) == 0 { // has no controller + continue + } + + // try to remove controller + doc.Controller = nil + + if len(doc.VerificationMethod) == 0 { + log.Logger().WithField(core.LogFieldDID, doc.ID.String()).Warnf("No verification method found in owned DID document") + continue + } + + if len(doc.CapabilityInvocation) == 0 { + // add all keys as capabilityInvocation keys + for _, vm := range doc.VerificationMethod { + doc.CapabilityInvocation.Add(vm) + } + } + + err = r.nutsDocumentManager.Update(auditContext, did, *doc) + if err != nil { + if !(errors.Is(err, resolver.ErrKeyNotFound)) { + log.Logger().WithError(err).WithField(core.LogFieldDID, did.String()).Error("Could not update owned DID document, continuing with next document") } } } - return nil } + +// migrateHistoryOwnedDIDNuts migrates did:nuts DIDs from the VDR key-value storage to SQL storage +// This ignores any DIDs that are not did:nuts. +func (r *Module) migrateHistoryOwnedDIDNuts(owned []did.DID) { + for _, id := range owned { + if id.Method != didnuts.MethodName { // skip non did:nuts + continue + } + err := r.Manager.(didsubject.DocumentMigration).MigrateDIDHistoryToSQL(id, id.String(), r.store.HistorySinceVersion) + if err != nil { + log.Logger().WithError(err).Errorf("Failed to migrate DID document history to SQL for %s", id) + } + } +} + +// migration is the signature each migration function in Module.migrations uses +// there is no error return, if something is fatal the function should panic +type migration func(owned []did.DID) diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index d37c759d0..6d4058cb5 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -86,6 +86,7 @@ func newVDRTestCtx(t *testing.T) vdrTestCtx { DB: db, MethodManagers: make(map[string]didsubject.MethodManager), } + vdr.config = DefaultConfig() resolverRouter.Register(didnuts.MethodName, &didnuts.Resolver{Store: mockStore}) return vdrTestCtx{ ctrl: ctrl, @@ -347,103 +348,130 @@ func TestVDR_Migrate(t *testing.T) { require.NoError(t, err) assert.Contains(t, msg, expected) } - - t.Run("ignores self-controlled documents", func(t *testing.T) { - t.Cleanup(func() { hook.Reset() }) + t.Run("ignores non did:nuts", func(t *testing.T) { ctx := newVDRTestCtx(t) - ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&did.Document{ID: TestDIDA}, nil, nil) - + testDIDWeb := did.MustParseDID("did:web:example.com") + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{testDIDWeb}, nil) err := ctx.vdr.Migrate() - - require.NoError(t, err) - // empty logs means all ok. - assert.Nil(t, hook.LastEntry()) + assert.NoError(t, err) + assert.Len(t, ctx.vdr.migrations, 2) // confirm its running allMigrations() that currently is only did:nuts }) - t.Run("makes documents self-controlled", func(t *testing.T) { - t.Cleanup(func() { hook.Reset() }) - ctx := newVDRTestCtx(t) - keyStore := nutsCrypto.NewMemoryCryptoInstance(t) - keyRef, publicKey, err := keyStore.New(ctx.ctx, didnuts.DIDKIDNamingFunc) - require.NoError(t, err) - methodID := did.MustParseDIDURL(keyRef.KID) - methodID.ID = TestDIDA.ID - vm, _ := did.NewVerificationMethod(methodID, ssi.JsonWebKey2020, TestDIDA, publicKey) - documentA := did.Document{Context: []interface{}{did.DIDContextV1URI()}, ID: TestDIDA, Controller: []did.DID{TestDIDB}} - documentA.AddAssertionMethod(vm) - ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&documentA, &resolver.DocumentMetadata{}, nil).AnyTimes() - ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&documentB, &resolver.DocumentMetadata{}, nil).AnyTimes() - ctx.mockDocumentManager.EXPECT().Update(gomock.Any(), TestDIDA, gomock.Any()).Return(nil) - - err = ctx.vdr.Migrate() + t.Run("controller migration", func(t *testing.T) { + controllerMigrationSetup := func(t *testing.T) vdrTestCtx { + t.Cleanup(func() { hook.Reset() }) + ctx := newVDRTestCtx(t) + ctx.vdr.migrations = map[string]migration{"remove controller": ctx.vdr.migrateRemoveControllerFromDIDNuts} + return ctx + } + t.Run("ignores self-controlled documents", func(t *testing.T) { + ctx := controllerMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&did.Document{ID: TestDIDA}, nil, nil) - require.NoError(t, err) - // empty logs means all ok. - assert.Nil(t, hook.LastEntry()) - }) - t.Run("deactivated is ignored", func(t *testing.T) { - t.Cleanup(func() { hook.Reset() }) - ctx := newVDRTestCtx(t) - ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(nil, nil, resolver.ErrDeactivated) + err := ctx.vdr.Migrate() - err := ctx.vdr.Migrate() + require.NoError(t, err) + // empty logs means all ok. + assert.Nil(t, hook.LastEntry()) + }) + t.Run("makes documents self-controlled", func(t *testing.T) { + ctx := controllerMigrationSetup(t) + keyStore := nutsCrypto.NewMemoryCryptoInstance(t) + keyRef, publicKey, err := keyStore.New(ctx.ctx, didnuts.DIDKIDNamingFunc) + require.NoError(t, err) + methodID := did.MustParseDIDURL(keyRef.KID) + methodID.ID = TestDIDA.ID + vm, _ := did.NewVerificationMethod(methodID, ssi.JsonWebKey2020, TestDIDA, publicKey) + documentA := did.Document{Context: []interface{}{did.DIDContextV1URI()}, ID: TestDIDA, Controller: []did.DID{TestDIDB}} + documentA.AddAssertionMethod(vm) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&documentA, &resolver.DocumentMetadata{}, nil).AnyTimes() + ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&documentB, &resolver.DocumentMetadata{}, nil).AnyTimes() + ctx.mockDocumentManager.EXPECT().Update(gomock.Any(), TestDIDA, gomock.Any()).Return(nil) + + err = ctx.vdr.Migrate() - require.NoError(t, err) - // empty logs means all ok. - assert.Nil(t, hook.LastEntry()) - }) - t.Run("no active controller is ignored", func(t *testing.T) { - t.Cleanup(func() { hook.Reset() }) - ctx := newVDRTestCtx(t) - ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&documentA, nil, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&did.Document{ID: TestDIDB}, nil, nil) + require.NoError(t, err) + // empty logs means all ok. + assert.Nil(t, hook.LastEntry()) + }) + t.Run("deactivated is ignored", func(t *testing.T) { + ctx := controllerMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(nil, nil, resolver.ErrDeactivated) - err := ctx.vdr.Migrate() + err := ctx.vdr.Migrate() - require.NoError(t, err) - // empty logs means all ok. - assert.Nil(t, hook.LastEntry()) - }) - t.Run("error is logged", func(t *testing.T) { - t.Cleanup(func() { hook.Reset() }) - ctx := newVDRTestCtx(t) - ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(nil, nil, assert.AnError) + require.NoError(t, err) + // empty logs means all ok. + assert.Nil(t, hook.LastEntry()) + }) + t.Run("no active controller is ignored", func(t *testing.T) { + ctx := controllerMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&documentA, nil, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&did.Document{ID: TestDIDB}, nil, nil) - err := ctx.vdr.Migrate() + err := ctx.vdr.Migrate() - require.NoError(t, err) - assertLog(t, "Could not update owned DID document, continuing with next document") - assertLog(t, "assert.AnError general error for testing") - }) - t.Run("no verification method is logged", func(t *testing.T) { - t.Cleanup(func() { hook.Reset() }) - ctx := newVDRTestCtx(t) - ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&did.Document{Controller: []did.DID{TestDIDB}}, nil, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&documentB, &resolver.DocumentMetadata{}, nil) + require.NoError(t, err) + // empty logs means all ok. + assert.Nil(t, hook.LastEntry()) + }) + t.Run("error is logged", func(t *testing.T) { + ctx := controllerMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(nil, nil, assert.AnError) - err := ctx.vdr.Migrate() + err := ctx.vdr.Migrate() - require.NoError(t, err) - assertLog(t, "No verification method found in owned DID document") + require.NoError(t, err) + assertLog(t, "Could not update owned DID document, continuing with next document") + assertLog(t, "assert.AnError general error for testing") + }) + t.Run("no verification method is logged", func(t *testing.T) { + ctx := controllerMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&did.Document{Controller: []did.DID{TestDIDB}}, nil, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&documentB, &resolver.DocumentMetadata{}, nil) + + err := ctx.vdr.Migrate() + + require.NoError(t, err) + assertLog(t, "No verification method found in owned DID document") + }) + t.Run("update error is logged", func(t *testing.T) { + ctx := controllerMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&documentA, &resolver.DocumentMetadata{}, nil).AnyTimes() + ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&documentB, &resolver.DocumentMetadata{}, nil).AnyTimes() + ctx.mockDocumentManager.EXPECT().Update(gomock.Any(), TestDIDA, gomock.Any()).Return(assert.AnError) + + err := ctx.vdr.Migrate() + + require.NoError(t, err) + assertLog(t, "Could not update owned DID document, continuing with next document") + assertLog(t, "assert.AnError general error for testing") + }) }) - t.Run("update error is logged", func(t *testing.T) { - t.Cleanup(func() { hook.Reset() }) - ctx := newVDRTestCtx(t) - ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) - ctx.mockStore.EXPECT().Resolve(TestDIDA, gomock.Any()).Return(&documentA, &resolver.DocumentMetadata{}, nil).AnyTimes() - ctx.mockStore.EXPECT().Resolve(TestDIDB, gomock.Any()).Return(&documentB, &resolver.DocumentMetadata{}, nil).AnyTimes() - ctx.mockDocumentManager.EXPECT().Update(gomock.Any(), TestDIDA, gomock.Any()).Return(assert.AnError) - err := ctx.vdr.Migrate() + t.Run("history migration", func(t *testing.T) { + historyMigrationSetup := func(t *testing.T) vdrTestCtx { + t.Cleanup(func() { hook.Reset() }) + ctx := newVDRTestCtx(t) + ctx.vdr.migrations = map[string]migration{"history migration": ctx.vdr.migrateHistoryOwnedDIDNuts} + return ctx + } + t.Run("logs error", func(t *testing.T) { + ctx := historyMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + ctx.mockStore.EXPECT().HistorySinceVersion(TestDIDA, 0).Return(nil, assert.AnError).AnyTimes() - require.NoError(t, err) - assertLog(t, "Could not update owned DID document, continuing with next document") - assertLog(t, "assert.AnError general error for testing") + err := ctx.vdr.Migrate() + + assert.NoError(t, err) + assertLog(t, "assert.AnError general error for testing") + }) }) } From 826aa348b6735fbc274cd7b55a2a890c48ec7051 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:25:57 +0200 Subject: [PATCH 03/72] migration: did:web for existing did:nuts (#3421) * migration: did:web for existing did:nuts * migration run in deterministic order --- e2e-tests/migration/main_test.go | 72 +++++++++++++----- storage/test.go | 2 +- vdr/didsubject/interface.go | 3 + vdr/didsubject/manager.go | 71 ++++++++++++++++++ vdr/didsubject/manager_test.go | 125 +++++++++++++++++++++++++++++++ vdr/vdr.go | 45 ++++++++--- vdr/vdr_test.go | 41 ++++++++-- 7 files changed, 325 insertions(+), 34 deletions(-) diff --git a/e2e-tests/migration/main_test.go b/e2e-tests/migration/main_test.go index 213a34b21..e877e8be9 100644 --- a/e2e-tests/migration/main_test.go +++ b/e2e-tests/migration/main_test.go @@ -22,24 +22,32 @@ package migration import ( "encoding/json" - did "github.com/nuts-foundation/go-did/did" - "github.com/stretchr/testify/assert" - "os" - "testing" - "time" - + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/storage/orm" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "os" + "strings" + "testing" ) +type manager struct { + DID *didsubject.SqlDIDManager + DOC *didsubject.SqlDIDDocumentManager +} + func Test_Migrations(t *testing.T) { db := storage.NewTestStorageEngineInDir(t, "./nodeA/data").GetSQLDatabase() + man := &manager{ + DID: didsubject.NewDIDManager(db), + DOC: didsubject.NewDIDDocumentManager(db), + } - DIDs, err := didsubject.NewDIDManager(db).All() + DIDs, err := man.DID.All() require.NoError(t, err) - require.Len(t, DIDs, 4) + require.Len(t, DIDs, 7) // 4 did:nuts, 3 did:web t.Run("vendor", func(t *testing.T) { // versions for did:nuts: @@ -51,13 +59,15 @@ func Test_Migrations(t *testing.T) { // // total 4 versions in SQL; latest has 2 services and 2 VMs id := did.MustParseDID(os.Getenv("VENDOR_DID")) - var doc orm.DidDocument - err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + doc, err := man.DOC.Latest(id, nil) require.NoError(t, err) assert.Equal(t, 3, doc.Version) assert.Len(t, doc.Services, 2) assert.Len(t, doc.VerificationMethods, 2) + + // migration: add did:web + EqualServices(t, man, doc) }) t.Run("org1", func(t *testing.T) { // versions for did:nuts: @@ -68,8 +78,7 @@ func Test_Migrations(t *testing.T) { // // total 4 versions in SQL; latest one has no controller, 2 services, and 1 VM id := did.MustParseDID(os.Getenv("ORG1_DID")) - var doc orm.DidDocument - err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + doc, err := man.DOC.Latest(id, nil) require.NoError(t, err) assert.Equal(t, 3, doc.Version) @@ -78,6 +87,9 @@ func Test_Migrations(t *testing.T) { didDoc := new(did.Document) require.NoError(t, json.Unmarshal([]byte(doc.Raw), didDoc)) assert.Empty(t, didDoc.Controller) + + // migration: add did:web + EqualServices(t, man, doc) }) t.Run("org2", func(t *testing.T) { // versions for did:nuts: @@ -88,13 +100,17 @@ func Test_Migrations(t *testing.T) { // // total 2 versions in SQL, migration stopped at LC5; no controller, 0 service, 0 VM id := did.MustParseDID(os.Getenv("ORG2_DID")) - var doc orm.DidDocument - err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + doc, err := man.DOC.Latest(id, nil) require.NoError(t, err) assert.Equal(t, 1, doc.Version) assert.Len(t, doc.Services, 0) assert.Len(t, doc.VerificationMethods, 0) + + // deactivated; has no did:web + dids, err := man.DID.FindBySubject(doc.DID.Subject) // migrated documents have subject == did:nuts:... + require.NoError(t, err) + assert.Len(t, dids, 1) }) t.Run("org3", func(t *testing.T) { // versions for did:nuts: @@ -102,12 +118,11 @@ func Test_Migrations(t *testing.T) { // - LC7: add service1 // - LC7: add verification method, conflicts with above // - LC9: add service2, solves conflict - // migration removes controller, total 5 versions in SQL + // migration removes controller // // total 5 versions in SQL; no controller, 2 services, 2 VMs id := did.MustParseDID(os.Getenv("ORG3_DID")) - var doc orm.DidDocument - err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error + doc, err := man.DOC.Latest(id, nil) require.NoError(t, err) assert.Equal(t, 4, doc.Version) @@ -116,5 +131,28 @@ func Test_Migrations(t *testing.T) { didDoc := new(did.Document) require.NoError(t, json.Unmarshal([]byte(doc.Raw), didDoc)) assert.Empty(t, didDoc.Controller) + + // migration: add did:web + EqualServices(t, man, doc) }) } + +func EqualServices(t *testing.T, man *manager, nutsDoc *orm.DidDocument) { + didWebPrefix := "did:web:nodeA%3A8080" + + dids, err := man.DID.FindBySubject(nutsDoc.DID.Subject) // migrated documents have subject == did:nuts:... + require.NoError(t, err) + assert.Len(t, dids, 2) + var webDoc *orm.DidDocument + for _, id := range dids { + if strings.HasPrefix(id.ID, "did:web:") { + webDoc, err = man.DOC.Latest(did.MustParseDID(id.ID), nil) + require.NoError(t, err) + } + } + assert.Equal(t, 0, webDoc.Version) + assert.Equal(t, len(nutsDoc.Services), len(webDoc.Services)) + for _, service := range webDoc.Services { + assert.True(t, strings.HasPrefix(service.ID, didWebPrefix)) + } +} diff --git a/storage/test.go b/storage/test.go index e6f0b432b..549b564cd 100644 --- a/storage/test.go +++ b/storage/test.go @@ -129,7 +129,7 @@ func NewTestInMemorySessionDatabase(t *testing.T) *InMemorySessionDatabase { func AddDIDtoSQLDB(t testing.TB, db *gorm.DB, dids ...did.DID) { for _, id := range dids { // use gorm EXEC since it accepts '?' as the argument placeholder for all DBs - require.NoError(t, db.Exec("INSERT INTO did (subject, id ) VALUES ( ?, ? )", id.String(), id.String(), id.String()).Error) + require.NoError(t, db.Exec("INSERT INTO did ( subject, id ) VALUES ( ?, ? )", id.String(), id.String(), id.String()).Error) } } diff --git a/vdr/didsubject/interface.go b/vdr/didsubject/interface.go index d718a9bb6..7d1d0fec3 100644 --- a/vdr/didsubject/interface.go +++ b/vdr/didsubject/interface.go @@ -141,6 +141,9 @@ type DocumentMigration interface { // It adds all versions of a DID Document up to a deactivated version. Any changes after a deactivation are not migrated. // getHistory retrieves the history of the DID since the requested version. MigrateDIDHistoryToSQL(id did.DID, subject string, getHistory func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error)) error + // MigrateAddWebToNuts checks if the provided did:nuts DID adds a did:web DID under the same subject if does not already have one. + // It does not check that id is a did:nuts DID + MigrateAddWebToNuts(ctx context.Context, id did.DID) error } // SubjectCreationOption links all create DIDs to the DID Subject diff --git a/vdr/didsubject/manager.go b/vdr/didsubject/manager.go index 334780e9b..992cea064 100644 --- a/vdr/didsubject/manager.go +++ b/vdr/didsubject/manager.go @@ -721,3 +721,74 @@ func (r *SqlManager) MigrateDIDHistoryToSQL(id did.DID, subject string, getHisto return nil }) } + +func (r *SqlManager) MigrateAddWebToNuts(ctx context.Context, id did.DID) error { + // get subject + // TODO: this should only run on migrations, so could use 'subject = id.String()' + var subject string + err := r.DB.Model(new(orm.DID)).Where("id = ?", id.String()).Select("subject").First(&subject).Error + if err != nil { + return err + } + + // check if subject has a did:web + subjectDIDs, err := r.ListDIDs(ctx, subject) + for _, subjectDID := range subjectDIDs { + if subjectDID.Method == "web" { + // already has a did:web + return nil + } + } + + // get latest did:nuts document + sqlDIDDocumentManager := NewDIDDocumentManager(r.DB) + nutsDoc, err := sqlDIDDocumentManager.Latest(id, nil) + if err != nil { + return err + } + + // don't add did:web if did:nuts is deactivated + nutsDidDoc, err := nutsDoc.ToDIDDocument() + if err != nil { + return err + } + if resolver.IsDeactivated(nutsDidDoc) { + return nil + } + + // create a did:web for this subject + webDoc, err := r.MethodManagers["web"].NewDocument(ctx, orm.AssertionKeyUsage()) + if err != nil { + return err + } + // add subject + webDID := orm.DID{ + ID: webDoc.DID.ID, + Subject: subject, + } + // rename services. only the DID part of the service.ID needs to be updates + webDoc.Services = make([]orm.Service, len(nutsDoc.Services)) + for i, ormService := range nutsDoc.Services { + service := new(did.Service) + err = json.Unmarshal(ormService.Data, service) + if err != nil { + return err + } + service.ID = ssi.MustParseURI(webDID.ID + "#" + service.ID.Fragment) + rawService, err := json.Marshal(service) + if err != nil { + return err + } + webDoc.Services[i] = orm.Service{ + ID: service.ID.String(), + Data: rawService, + } + } + // store did:web + _, err = sqlDIDDocumentManager.CreateOrUpdate(webDID, webDoc.VerificationMethods, webDoc.Services) + if err != nil { + return err + } + + return nil +} diff --git a/vdr/didsubject/manager_test.go b/vdr/didsubject/manager_test.go index 9134b71e6..4e85f92c4 100644 --- a/vdr/didsubject/manager_test.go +++ b/vdr/didsubject/manager_test.go @@ -448,9 +448,13 @@ type testMethod struct { committed bool error error method string + document *orm.DidDocument } func (t testMethod) NewDocument(_ context.Context, _ orm.DIDKeyFlags) (*orm.DidDocument, error) { + if t.document != nil { + return t.document, nil + } method := t.method if method == "" { method = "example" @@ -508,6 +512,7 @@ func TestSqlManager_MigrateDIDHistoryToSQL(t *testing.T) { ormMigrateUpdate := orm.MigrationDocument{Raw: rawDocUpdate, Created: created, Updated: created.Add(2 * time.Second), Version: 1} ormMigrateDeactivate := orm.MigrationDocument{Raw: rawDocDeactivate, Created: created, Updated: created.Add(2 * time.Second), Version: 1} ormDocNew, err := ormMigrateNew.ToORMDocument(subject) + require.NoError(t, err) ormDocUpdate, _ := ormMigrateUpdate.ToORMDocument(subject) ormDocDeactivate, _ := ormMigrateDeactivate.ToORMDocument(subject) equal := func(t *testing.T, o1, o2 orm.DidDocument) { @@ -594,3 +599,123 @@ func TestSqlManager_MigrateDIDHistoryToSQL(t *testing.T) { assert.EqualError(t, err, "test") }) } + +func TestSqlManager_MigrateAddWebToNuts(t *testing.T) { + didNuts := did.MustParseDID("did:nuts:test") + didWeb := did.MustParseDID("did:web:example.com") + nutsDoc := generateTestORMDoc(t, didNuts, didNuts.String(), true) + webDoc := generateTestORMDoc(t, didWeb, didNuts.String(), false) // don't add service to check it gets migrated properly + + var err error + auditContext := audit.Context(context.Background(), "system", "VDR", "migrate_add_did:web_to_did:nuts") + + t.Run("ok", func(t *testing.T) { + db := testDB(t) + _, err = NewDIDDocumentManager(db).CreateOrUpdate(nutsDoc.DID, nutsDoc.VerificationMethods, nutsDoc.Services) + require.NoError(t, err) + m := SqlManager{DB: db, MethodManagers: map[string]MethodManager{ + "web": testMethod{document: &webDoc}, + }, PreferredOrder: []string{"web"}} + + // create did:web + err = m.MigrateAddWebToNuts(auditContext, didNuts) + assert.NoError(t, err) + + dids, err := NewDIDManager(db).FindBySubject(didNuts.String()) + assert.NoError(t, err) + require.Len(t, dids, 2) + assert.Equal(t, didNuts.String(), dids[0].ID) + assert.Equal(t, didWeb.String(), dids[1].ID) + + docNuts, err := NewDIDDocumentManager(db).Latest(didNuts, nil) + require.NoError(t, err) + docWeb, err := NewDIDDocumentManager(db).Latest(didWeb, nil) + require.NoError(t, err) + assert.Equal(t, len(docNuts.Services), len(docWeb.Services)) + assert.Equal(t, didWeb.String()+"#service-1", docWeb.Services[0].ID) + }) + t.Run("ok - already has did:web", func(t *testing.T) { + db := testDB(t) + _, err = NewDIDDocumentManager(db).CreateOrUpdate(nutsDoc.DID, nutsDoc.VerificationMethods, nutsDoc.Services) + require.NoError(t, err) + m := SqlManager{DB: db, MethodManagers: map[string]MethodManager{ + "web": testMethod{document: &webDoc}, + }, PreferredOrder: []string{"web"}} + + // migration 1; create did:web + err = m.MigrateAddWebToNuts(auditContext, didNuts) + assert.NoError(t, err) + + dids, err := NewDIDManager(db).FindBySubject(didNuts.String()) + assert.NoError(t, err) + require.Len(t, dids, 2) + + // migration 2; already has a did:web + err = m.MigrateAddWebToNuts(auditContext, didNuts) + assert.NoError(t, err) + + dids2, err := NewDIDManager(db).FindBySubject(didNuts.String()) + assert.NoError(t, err) + require.Len(t, dids2, 2) + assert.Equal(t, dids, dids2) + }) + t.Run("ok - deactivated", func(t *testing.T) { + db := testDB(t) + _, err = NewDIDDocumentManager(db).CreateOrUpdate(nutsDoc.DID, nil, nil) + require.NoError(t, err) + m := SqlManager{DB: db} + + // migrate is a noop + err = m.MigrateAddWebToNuts(auditContext, didNuts) + assert.NoError(t, err) + + dids, err := NewDIDManager(db).FindBySubject(didNuts.String()) + assert.NoError(t, err) + require.Len(t, dids, 1) + assert.Equal(t, didNuts.String(), dids[0].ID) + }) + t.Run("error - did not found", func(t *testing.T) { + db := testDB(t) + m := SqlManager{DB: db} + + // empty db + err = m.MigrateAddWebToNuts(auditContext, didNuts) + + assert.ErrorIs(t, err, gorm.ErrRecordNotFound) + }) + t.Run("error - doc not found", func(t *testing.T) { + db := testDB(t) + storage.AddDIDtoSQLDB(t, db, didNuts) // only add did, not the doc + m := SqlManager{DB: db} + + // migrate is a noop + err = m.MigrateAddWebToNuts(auditContext, didNuts) + + assert.ErrorIs(t, err, gorm.ErrRecordNotFound) + }) +} + +func generateTestORMDoc(t *testing.T, id did.DID, subject string, addService bool) orm.DidDocument { + // verification method + vmID := did.MustParseDIDURL(id.String() + "#key-1") + key, _ := spi.GenerateKeyPair() + vm, err := did.NewVerificationMethod(vmID, ssi.JsonWebKey2020, id, key.Public()) + require.NoError(t, err) + // service + var service did.Service + if addService { + service = did.Service{ + ID: ssi.MustParseURI(id.String() + "#service-1"), + Type: "test", + ServiceEndpoint: "https://example.com", + } + } + // generate and parse document + didDoc := did.Document{ID: id, VerificationMethod: did.VerificationMethods{vm}, CapabilityInvocation: []did.VerificationRelationship{{VerificationMethod: vm}}, Service: []did.Service{service}} + rawDoc, err := json.Marshal(didDoc) + require.NoError(t, err) + now := time.Now() + ormDoc, err := orm.MigrationDocument{Raw: rawDoc, Created: now, Updated: now, Version: 0}.ToORMDocument(subject) + require.NoError(t, err) + return ormDoc +} diff --git a/vdr/vdr.go b/vdr/vdr.go index dcd4234b5..1924a369c 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -78,7 +78,7 @@ type Module struct { storageInstance storage.Engine eventManager events.Event // migrations are registered functions to simplify testing - migrations map[string]migration + migrations []migration // new style DID management didsubject.Manager @@ -369,18 +369,28 @@ func (r *Module) Migrate() error { // only migrate if did:nuts is activated on the node if slices.Contains(r.SupportedMethods(), "nuts") { - for name, migrate := range r.migrations { - log.Logger().Infof("Running did:nuts migration: '%s'", name) - migrate(owned) + for _, m := range r.migrations { + log.Logger().Infof("Running did:nuts migration: '%s'", m.name) + m.migrate(owned) } } return nil } -func (r *Module) allMigrations() map[string]migration { - return map[string]migration{ // key will be printed as description of the migration - "remove controller": r.migrateRemoveControllerFromDIDNuts, // must come before migrateHistoryOwnedDIDNuts so controller removal is also migrated. - "document history": r.migrateHistoryOwnedDIDNuts, +// migration is the signature each migration function in Module.migrations uses +// there is no error return, if something is fatal the function should panic +type migrationFn func(owned []did.DID) + +type migration struct { + migrate migrationFn + name string +} + +func (r *Module) allMigrations() []migration { + return []migration{ // key will be printed as description of the migration + {r.migrateRemoveControllerFromDIDNuts, "remove controller"}, // must come before migrateHistoryOwnedDIDNuts so controller removal is also migrated. + {r.migrateHistoryOwnedDIDNuts, "document history"}, + {r.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}, // must come after migrateHistoryOwnedDIDNuts since it acts on the SQL store. } } @@ -442,6 +452,19 @@ func (r *Module) migrateHistoryOwnedDIDNuts(owned []did.DID) { } } -// migration is the signature each migration function in Module.migrations uses -// there is no error return, if something is fatal the function should panic -type migration func(owned []did.DID) +func (r *Module) migrateAddDIDWebToOwnedDIDNuts(owned []did.DID) { + if !slices.Contains(r.SupportedMethods(), "web") { + log.Logger().Info("did:web not in supported did methods. Abort migration.") + return + } + auditContext := audit.Context(context.Background(), "system", ModuleName, "migrate_add_did:web_to_did:nuts") + for _, id := range owned { + if id.Method != didnuts.MethodName { // skip non did:nuts + continue + } + err := r.Manager.(didsubject.DocumentMigration).MigrateAddWebToNuts(auditContext, id) + if err != nil { + log.Logger().WithError(err).Errorf("Failed to add a did:web DID for %s", id) + } + } +} diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index 6d4058cb5..7b31545b6 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -54,7 +54,7 @@ import ( // testCtx contains the controller and mocks needed fot testing the Manipulator type vdrTestCtx struct { ctrl *gomock.Controller - vdr Module + vdr *Module mockStore *didstore.MockStore mockNetwork *network.MockTransactions keyStore nutsCrypto.KeyStore @@ -90,7 +90,7 @@ func newVDRTestCtx(t *testing.T) vdrTestCtx { resolverRouter.Register(didnuts.MethodName, &didnuts.Resolver{Store: mockStore}) return vdrTestCtx{ ctrl: ctrl, - vdr: *vdr, + vdr: vdr, mockAmbassador: mockAmbassador, mockStore: mockStore, mockNetwork: mockNetwork, @@ -354,13 +354,13 @@ func TestVDR_Migrate(t *testing.T) { ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{testDIDWeb}, nil) err := ctx.vdr.Migrate() assert.NoError(t, err) - assert.Len(t, ctx.vdr.migrations, 2) // confirm its running allMigrations() that currently is only did:nuts + assert.Len(t, ctx.vdr.migrations, 3) // confirm its running allMigrations() that currently is only did:nuts }) t.Run("controller migration", func(t *testing.T) { controllerMigrationSetup := func(t *testing.T) vdrTestCtx { t.Cleanup(func() { hook.Reset() }) ctx := newVDRTestCtx(t) - ctx.vdr.migrations = map[string]migration{"remove controller": ctx.vdr.migrateRemoveControllerFromDIDNuts} + ctx.vdr.migrations = []migration{{ctx.vdr.migrateRemoveControllerFromDIDNuts, "remove controller"}} return ctx } t.Run("ignores self-controlled documents", func(t *testing.T) { @@ -459,7 +459,7 @@ func TestVDR_Migrate(t *testing.T) { historyMigrationSetup := func(t *testing.T) vdrTestCtx { t.Cleanup(func() { hook.Reset() }) ctx := newVDRTestCtx(t) - ctx.vdr.migrations = map[string]migration{"history migration": ctx.vdr.migrateHistoryOwnedDIDNuts} + ctx.vdr.migrations = []migration{{ctx.vdr.migrateHistoryOwnedDIDNuts, "history migration"}} return ctx } t.Run("logs error", func(t *testing.T) { @@ -473,6 +473,37 @@ func TestVDR_Migrate(t *testing.T) { assertLog(t, "assert.AnError general error for testing") }) }) + + t.Run("add did:web to subject", func(t *testing.T) { + didwebMigrationSetup := func(t *testing.T) vdrTestCtx { + t.Cleanup(func() { hook.Reset() }) + ctx := newVDRTestCtx(t) + ctx.vdr.migrations = []migration{{ctx.vdr.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}} + return ctx + } + t.Run("web not in supported methods", func(t *testing.T) { + logrus.StandardLogger().Level = logrus.InfoLevel + defer func() { logrus.StandardLogger().Level = logrus.WarnLevel }() + ctx := didwebMigrationSetup(t) + ctx.vdr.migrations = []migration{{ctx.vdr.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}} + ctx.vdr.config.DIDMethods = []string{"nuts"} + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + + err := ctx.vdr.Migrate() + + require.NoError(t, err) + assertLog(t, "did:web not in supported did methods. Abort migration.") + }) + t.Run("logs error", func(t *testing.T) { + ctx := didwebMigrationSetup(t) + ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) + + err := ctx.vdr.Migrate() + + assert.NoError(t, err) + assertLog(t, "Failed to add a did:web DID for did:nuts:") // test sql store does not contain TestDIDA + }) + }) } type roundTripperFunc func(*http.Request) (*http.Response, error) From 5aa37efa6534a3f775f7eb6dc9e4475ed8eeafda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:49:25 +0200 Subject: [PATCH 04/72] Bump google.golang.org/grpc from 1.67.0 to 1.67.1 (#3431) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.67.0 to 1.67.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.67.0...v1.67.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7886813a0..3eca4aaa7 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( go.uber.org/mock v0.4.0 golang.org/x/crypto v0.27.0 golang.org/x/time v0.6.0 - google.golang.org/grpc v1.67.0 + google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.34.2 gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index de75e72b3..39f04019f 100644 --- a/go.sum +++ b/go.sum @@ -867,8 +867,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= -google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From ee55a89d5a6c6a80a7716e88e00e3db9059a65a9 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 2 Oct 2024 08:59:48 +0200 Subject: [PATCH 05/72] fixes for rfc021 client status codes (#3433) --- auth/api/iam/api.go | 14 +++++++----- auth/client/iam/client.go | 26 +++++++++++++++++++++- auth/client/iam/client_test.go | 11 +++++++++ auth/client/iam/openid4vp.go | 8 +++++-- auth/client/iam/openid4vp_test.go | 22 +++++++++++------- docs/_static/auth/v2.yaml | 7 +++--- vdr/didsubject/mock.go | 37 +++++++++++++++++++++++++++++++ 7 files changed, 106 insertions(+), 19 deletions(-) diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index b3f61b156..f8d660bed 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -27,33 +27,34 @@ import ( "encoding/json" "errors" "fmt" - "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/nuts-foundation/nuts-node/http/cache" - "github.com/nuts-foundation/nuts-node/http/user" - "github.com/nuts-foundation/nuts-node/vdr/didsubject" "html/template" "net/http" "net/url" "strings" "time" + "github.com/labstack/echo/v4" + "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/auth/api/iam/assets" + iamclient "github.com/nuts-foundation/nuts-node/auth/client/iam" "github.com/nuts-foundation/nuts-node/auth/log" "github.com/nuts-foundation/nuts-node/auth/oauth" "github.com/nuts-foundation/nuts-node/core" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" nutsHttp "github.com/nuts-foundation/nuts-node/http" + "github.com/nuts-foundation/nuts-node/http/cache" + "github.com/nuts-foundation/nuts-node/http/user" "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/policy" "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vdr" + "github.com/nuts-foundation/nuts-node/vdr/didsubject" "github.com/nuts-foundation/nuts-node/vdr/resolver" ) @@ -202,6 +203,9 @@ func (r Wrapper) ResolveStatusCode(err error) int { resolver.ErrDIDNotManagedByThisNode: http.StatusBadRequest, pe.ErrNoCredentials: http.StatusPreconditionFailed, didsubject.ErrSubjectNotFound: http.StatusNotFound, + iamclient.ErrInvalidClientCall: http.StatusBadRequest, + iamclient.ErrBadGateway: http.StatusBadGateway, + iamclient.ErrPreconditionFailed: http.StatusPreconditionFailed, }) } diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go index 1dc1d9b05..68a216ad4 100644 --- a/auth/client/iam/client.go +++ b/auth/client/iam/client.go @@ -22,6 +22,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" @@ -40,6 +41,12 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/pe" ) +// ErrInvalidClientCall is returned when the node makes a http call as client based on wrong information passed by the client. +var ErrInvalidClientCall = errors.New("invalid client call") + +// ErrBadGateway is returned when the node makes a http call as client and the upstream returns an unexpected result. +var ErrBadGateway = errors.New("upstream returned unexpected result") + // HTTPClient holds the server address and other basic settings for the http client type HTTPClient struct { strictMode bool @@ -63,7 +70,14 @@ func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, oauth } var metadata oauth.AuthorizationServerMetadata if err = hb.doGet(ctx, metadataURL.String(), &metadata); err != nil { - return nil, err + // if this is a core.HttpError and the status code >= 500 then we want the caller to receive a 502 Bad Gateway + // we do this by changing the status code of the error + // any other error should result in a 400 Bad Request + if httpErr, ok := err.(core.HttpError); ok && httpErr.StatusCode >= 500 { + httpErr.StatusCode = http.StatusBadGateway + return nil, httpErr + } + return nil, errors.Join(ErrInvalidClientCall, err) } return &metadata, err } @@ -93,6 +107,16 @@ func (hb HTTPClient) PresentationDefinition(ctx context.Context, presentationDef return nil, err } var presentationDefinition pe.PresentationDefinition + err = hb.doRequest(ctx, request, &presentationDefinition) + if err != nil { + // a 404 (defined by scope) should result in a 400 for the client + // any other error should result in a 502 Bad Gateway + if httpErr, ok := err.(core.HttpError); ok && httpErr.StatusCode == 404 { + return nil, errors.Join(ErrInvalidClientCall, err) + } + return nil, errors.Join(ErrBadGateway, err) + } + return &presentationDefinition, hb.doRequest(ctx, request, &presentationDefinition) } diff --git a/auth/client/iam/client_test.go b/auth/client/iam/client_test.go index 3c37deed7..bb205eff0 100644 --- a/auth/client/iam/client_test.go +++ b/auth/client/iam/client_test.go @@ -78,6 +78,17 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) { assert.Equal(t, "GET", handler.Request.Method) assert.Equal(t, "/.well-known/oauth-authorization-server/iam/123", handler.Request.URL.Path) }) + t.Run("error - server error changes status code to 502", func(t *testing.T) { + handler := http2.Handler{StatusCode: http.StatusInternalServerError} + tlsServer, client := testServerAndClient(t, &handler) + + _, err := client.OAuthAuthorizationServerMetadata(ctx, tlsServer.URL) + + require.Error(t, err) + httpErr, ok := err.(core.HttpError) + require.True(t, ok) + assert.Equal(t, http.StatusBadGateway, httpErr.StatusCode) + }) } func TestHTTPClient_PresentationDefinition(t *testing.T) { diff --git a/auth/client/iam/openid4vp.go b/auth/client/iam/openid4vp.go index 437cd9543..5fc02e084 100644 --- a/auth/client/iam/openid4vp.go +++ b/auth/client/iam/openid4vp.go @@ -22,6 +22,7 @@ package iam import ( "context" "encoding/json" + "errors" "fmt" "github.com/nuts-foundation/nuts-node/http/client" "github.com/nuts-foundation/nuts-node/vcr/credential" @@ -46,6 +47,9 @@ import ( "github.com/nuts-foundation/nuts-node/vdr/resolver" ) +// ErrPreconditionFailed is returned when a precondition is not met. +var ErrPreconditionFailed = errors.New("precondition failed") + var _ Client = (*OpenID4VPClient)(nil) type OpenID4VPClient struct { @@ -282,7 +286,7 @@ func (c *OpenID4VPClient) RequestRFC021AccessToken(ctx context.Context, clientID for key := range maps.Keys(allMethods) { availableMethods = append(availableMethods, key) } - return nil, fmt.Errorf("did method mismatch, requested: %v, available: %v", metadata.DIDMethodsSupported, availableMethods) + return nil, errors.Join(ErrPreconditionFailed, fmt.Errorf("did method mismatch, requested: %v, available: %v", metadata.DIDMethodsSupported, availableMethods)) } // each additional credential can be used by each DID @@ -320,7 +324,7 @@ func (c *OpenID4VPClient) RequestRFC021AccessToken(ctx context.Context, clientID } dpopHeader, dpopKid, err = c.dpop(ctx, *subjectDID, *request) if err != nil { - return nil, fmt.Errorf("failed tocreate DPoP header: %w", err) + return nil, fmt.Errorf("failed to create DPoP header: %w", err) } } diff --git a/auth/client/iam/openid4vp_test.go b/auth/client/iam/openid4vp_test.go index 7f106c53b..13df76769 100644 --- a/auth/client/iam/openid4vp_test.go +++ b/auth/client/iam/openid4vp_test.go @@ -230,7 +230,8 @@ func TestIAMClient_AuthorizationServerMetadata(t *testing.T) { _, err := ctx.client.AuthorizationServerMetadata(context.Background(), ctx.tlsServer.URL) require.Error(t, err) - assert.EqualError(t, err, "failed to retrieve remote OAuth Authorization Server metadata: server returned HTTP 404 (expected: 200)") + assert.ErrorIs(t, err, ErrInvalidClientCall) + assert.ErrorContains(t, err, "server returned HTTP 404 (expected: 200)") }) } @@ -275,7 +276,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { response, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) - assert.EqualError(t, err, "did method mismatch, requested: [other], available: [test]") + require.Error(t, err) + assert.ErrorIs(t, err, ErrPreconditionFailed) + assert.ErrorContains(t, err, "did method mismatch, requested: [other], available: [test]") assert.Nil(t, response) }) t.Run("with additional credentials", func(t *testing.T) { @@ -357,8 +360,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) - assert.Error(t, err) - assert.EqualError(t, err, "failed to retrieve presentation definition: server returned HTTP 404 (expected: 200)") + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidClientCall) + assert.ErrorContains(t, err, "server returned HTTP 404 (expected: 200)") }) t.Run("error - failed to get authorization server metadata", func(t *testing.T) { ctx := createClientServerTestContext(t) @@ -366,8 +370,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) - assert.Error(t, err) - assert.EqualError(t, err, "failed to retrieve remote OAuth Authorization Server metadata: server returned HTTP 404 (expected: 200)") + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidClientCall) + assert.ErrorContains(t, err, "server returned HTTP 404 (expected: 200)") }) t.Run("error - faulty presentation definition", func(t *testing.T) { ctx := createClientServerTestContext(t) @@ -379,8 +384,9 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) - assert.Error(t, err) - assert.EqualError(t, err, "failed to retrieve presentation definition: unable to unmarshal response: unexpected end of JSON input") + require.Error(t, err) + assert.ErrorIs(t, err, ErrBadGateway) + assert.ErrorContains(t, err, "unable to unmarshal response: unexpected end of JSON input") }) t.Run("error - failed to build vp", func(t *testing.T) { ctx := createClientServerTestContext(t) diff --git a/docs/_static/auth/v2.yaml b/docs/_static/auth/v2.yaml index f772b31ef..a31302e74 100644 --- a/docs/_static/auth/v2.yaml +++ b/docs/_static/auth/v2.yaml @@ -14,9 +14,10 @@ paths: It'll initiate a s2s (RFC021) flow. error returns: - * 400 - one of the parameters has the wrong format or an OAuth error occurred - * 412 - the organization wallet does not contain the correct credentials - * 503 - the authorizer could not be reached or returned an error + * 400 - one of the parameters has the wrong format, an OAuth error occurred, or the http client calling the authorizer returned an error due to incorrect input + * 412 - the organization wallet does not contain the correct credentials or doesn't support the right DID methods + * 502 - the authorizer returned an error + * 503 - the authorizer could not be reached tags: - auth parameters: diff --git a/vdr/didsubject/mock.go b/vdr/didsubject/mock.go index d94d10b2a..564ea3395 100644 --- a/vdr/didsubject/mock.go +++ b/vdr/didsubject/mock.go @@ -336,6 +336,43 @@ func (mr *MockManagerMockRecorder) UpdateService(ctx, subject, serviceID, servic return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateService", reflect.TypeOf((*MockManager)(nil).UpdateService), ctx, subject, serviceID, service) } +// MockDocumentMigration is a mock of DocumentMigration interface. +type MockDocumentMigration struct { + ctrl *gomock.Controller + recorder *MockDocumentMigrationMockRecorder +} + +// MockDocumentMigrationMockRecorder is the mock recorder for MockDocumentMigration. +type MockDocumentMigrationMockRecorder struct { + mock *MockDocumentMigration +} + +// NewMockDocumentMigration creates a new mock instance. +func NewMockDocumentMigration(ctrl *gomock.Controller) *MockDocumentMigration { + mock := &MockDocumentMigration{ctrl: ctrl} + mock.recorder = &MockDocumentMigrationMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDocumentMigration) EXPECT() *MockDocumentMigrationMockRecorder { + return m.recorder +} + +// MigrateDIDHistoryToSQL mocks base method. +func (m *MockDocumentMigration) MigrateDIDHistoryToSQL(id did.DID, subject string, getHistory func(did.DID, int) ([]orm.MigrationDocument, error)) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MigrateDIDHistoryToSQL", id, subject, getHistory) + ret0, _ := ret[0].(error) + return ret0 +} + +// MigrateDIDHistoryToSQL indicates an expected call of MigrateDIDHistoryToSQL. +func (mr *MockDocumentMigrationMockRecorder) MigrateDIDHistoryToSQL(id, subject, getHistory any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MigrateDIDHistoryToSQL", reflect.TypeOf((*MockDocumentMigration)(nil).MigrateDIDHistoryToSQL), id, subject, getHistory) +} + // MockCreationOptions is a mock of CreationOptions interface. type MockCreationOptions struct { ctrl *gomock.Controller From ceea91e42f8833e94021202c8fe549c094998951 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:22:12 +0200 Subject: [PATCH 06/72] Tools: protobuf (#3436) * Tools: upgrade protobuf --- makefile | 4 +- network/transport/grpc/testprotocol.pb.go | 8 +- .../transport/grpc/testprotocol_grpc.pb.go | 88 +++++++------------ network/transport/v2/protocol.pb.go | 32 +++---- network/transport/v2/protocol_grpc.pb.go | 88 +++++++------------ 5 files changed, 84 insertions(+), 136 deletions(-) diff --git a/makefile b/makefile index 4e9f25e0d..eae9c441f 100644 --- a/makefile +++ b/makefile @@ -4,9 +4,9 @@ run-generators: gen-mocks gen-api gen-protobuf install-tools: go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.3.0 - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.1 + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2 go install go.uber.org/mock/mockgen@v0.4.0 - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 gen-mocks: diff --git a/network/transport/grpc/testprotocol.pb.go b/network/transport/grpc/testprotocol.pb.go index 263a659ec..dc9d6f9dc 100644 --- a/network/transport/grpc/testprotocol.pb.go +++ b/network/transport/grpc/testprotocol.pb.go @@ -17,8 +17,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.1 -// protoc v4.25.3 +// protoc-gen-go v1.34.2 +// protoc v5.27.3 // source: transport/grpc/testprotocol.proto package grpc @@ -116,7 +116,7 @@ func file_transport_grpc_testprotocol_proto_rawDescGZIP() []byte { } var file_transport_grpc_testprotocol_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_grpc_testprotocol_proto_goTypes = []interface{}{ +var file_transport_grpc_testprotocol_proto_goTypes = []any{ (*TestMessage)(nil), // 0: grpc.TestMessage } var file_transport_grpc_testprotocol_proto_depIdxs = []int32{ @@ -135,7 +135,7 @@ func file_transport_grpc_testprotocol_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_transport_grpc_testprotocol_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_transport_grpc_testprotocol_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*TestMessage); i { case 0: return &v.state diff --git a/network/transport/grpc/testprotocol_grpc.pb.go b/network/transport/grpc/testprotocol_grpc.pb.go index b208b01b2..0461d719f 100644 --- a/network/transport/grpc/testprotocol_grpc.pb.go +++ b/network/transport/grpc/testprotocol_grpc.pb.go @@ -17,8 +17,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.3 +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.27.3 // source: transport/grpc/testprotocol.proto package grpc @@ -32,8 +32,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( Test_DoStuff_FullMethodName = "/grpc.Test/DoStuff" @@ -43,7 +43,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type TestClient interface { - DoStuff(ctx context.Context, opts ...grpc.CallOption) (Test_DoStuffClient, error) + DoStuff(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TestMessage, TestMessage], error) } type testClient struct { @@ -54,51 +54,37 @@ func NewTestClient(cc grpc.ClientConnInterface) TestClient { return &testClient{cc} } -func (c *testClient) DoStuff(ctx context.Context, opts ...grpc.CallOption) (Test_DoStuffClient, error) { - stream, err := c.cc.NewStream(ctx, &Test_ServiceDesc.Streams[0], Test_DoStuff_FullMethodName, opts...) +func (c *testClient) DoStuff(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TestMessage, TestMessage], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Test_ServiceDesc.Streams[0], Test_DoStuff_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &testDoStuffClient{stream} + x := &grpc.GenericClientStream[TestMessage, TestMessage]{ClientStream: stream} return x, nil } -type Test_DoStuffClient interface { - Send(*TestMessage) error - Recv() (*TestMessage, error) - grpc.ClientStream -} - -type testDoStuffClient struct { - grpc.ClientStream -} - -func (x *testDoStuffClient) Send(m *TestMessage) error { - return x.ClientStream.SendMsg(m) -} - -func (x *testDoStuffClient) Recv() (*TestMessage, error) { - m := new(TestMessage) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Test_DoStuffClient = grpc.BidiStreamingClient[TestMessage, TestMessage] // TestServer is the server API for Test service. // All implementations should embed UnimplementedTestServer -// for forward compatibility +// for forward compatibility. type TestServer interface { - DoStuff(Test_DoStuffServer) error + DoStuff(grpc.BidiStreamingServer[TestMessage, TestMessage]) error } -// UnimplementedTestServer should be embedded to have forward compatible implementations. -type UnimplementedTestServer struct { -} +// UnimplementedTestServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTestServer struct{} -func (UnimplementedTestServer) DoStuff(Test_DoStuffServer) error { +func (UnimplementedTestServer) DoStuff(grpc.BidiStreamingServer[TestMessage, TestMessage]) error { return status.Errorf(codes.Unimplemented, "method DoStuff not implemented") } +func (UnimplementedTestServer) testEmbeddedByValue() {} // UnsafeTestServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to TestServer will @@ -108,34 +94,22 @@ type UnsafeTestServer interface { } func RegisterTestServer(s grpc.ServiceRegistrar, srv TestServer) { + // If the following call pancis, it indicates UnimplementedTestServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&Test_ServiceDesc, srv) } func _Test_DoStuff_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(TestServer).DoStuff(&testDoStuffServer{stream}) -} - -type Test_DoStuffServer interface { - Send(*TestMessage) error - Recv() (*TestMessage, error) - grpc.ServerStream -} - -type testDoStuffServer struct { - grpc.ServerStream + return srv.(TestServer).DoStuff(&grpc.GenericServerStream[TestMessage, TestMessage]{ServerStream: stream}) } -func (x *testDoStuffServer) Send(m *TestMessage) error { - return x.ServerStream.SendMsg(m) -} - -func (x *testDoStuffServer) Recv() (*TestMessage, error) { - m := new(TestMessage) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Test_DoStuffServer = grpc.BidiStreamingServer[TestMessage, TestMessage] // Test_ServiceDesc is the grpc.ServiceDesc for Test service. // It's only intended for direct use with grpc.RegisterService, diff --git a/network/transport/v2/protocol.pb.go b/network/transport/v2/protocol.pb.go index 5af2f30ef..061eff71c 100644 --- a/network/transport/v2/protocol.pb.go +++ b/network/transport/v2/protocol.pb.go @@ -17,8 +17,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.1 -// protoc v4.25.3 +// protoc-gen-go v1.34.2 +// protoc v5.27.3 // source: transport/v2/protocol.proto package v2 @@ -1051,7 +1051,7 @@ func file_transport_v2_protocol_proto_rawDescGZIP() []byte { } var file_transport_v2_protocol_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_transport_v2_protocol_proto_goTypes = []interface{}{ +var file_transport_v2_protocol_proto_goTypes = []any{ (*Envelope)(nil), // 0: v2.Envelope (*Transaction)(nil), // 1: v2.Transaction (*Gossip)(nil), // 2: v2.Gossip @@ -1090,7 +1090,7 @@ func file_transport_v2_protocol_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_transport_v2_protocol_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Envelope); i { case 0: return &v.state @@ -1102,7 +1102,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*Transaction); i { case 0: return &v.state @@ -1114,7 +1114,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*Gossip); i { case 0: return &v.state @@ -1126,7 +1126,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*State); i { case 0: return &v.state @@ -1138,7 +1138,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*TransactionSet); i { case 0: return &v.state @@ -1150,7 +1150,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*TransactionListQuery); i { case 0: return &v.state @@ -1162,7 +1162,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*TransactionRangeQuery); i { case 0: return &v.state @@ -1174,7 +1174,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*TransactionList); i { case 0: return &v.state @@ -1186,7 +1186,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*TransactionPayloadQuery); i { case 0: return &v.state @@ -1198,7 +1198,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*TransactionPayload); i { case 0: return &v.state @@ -1210,7 +1210,7 @@ func file_transport_v2_protocol_proto_init() { return nil } } - file_transport_v2_protocol_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_transport_v2_protocol_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*Diagnostics); i { case 0: return &v.state @@ -1223,7 +1223,7 @@ func file_transport_v2_protocol_proto_init() { } } } - file_transport_v2_protocol_proto_msgTypes[0].OneofWrappers = []interface{}{ + file_transport_v2_protocol_proto_msgTypes[0].OneofWrappers = []any{ (*Envelope_Gossip)(nil), (*Envelope_DiagnosticsBroadcast)(nil), (*Envelope_State)(nil), @@ -1234,7 +1234,7 @@ func file_transport_v2_protocol_proto_init() { (*Envelope_TransactionList)(nil), (*Envelope_TransactionPayload)(nil), } - file_transport_v2_protocol_proto_msgTypes[1].OneofWrappers = []interface{}{} + file_transport_v2_protocol_proto_msgTypes[1].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/network/transport/v2/protocol_grpc.pb.go b/network/transport/v2/protocol_grpc.pb.go index 24eb0383b..56c7a7054 100644 --- a/network/transport/v2/protocol_grpc.pb.go +++ b/network/transport/v2/protocol_grpc.pb.go @@ -17,8 +17,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.3 +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.27.3 // source: transport/v2/protocol.proto package v2 @@ -32,8 +32,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( Protocol_Stream_FullMethodName = "/v2.Protocol/Stream" @@ -43,7 +43,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type ProtocolClient interface { - Stream(ctx context.Context, opts ...grpc.CallOption) (Protocol_StreamClient, error) + Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Envelope, Envelope], error) } type protocolClient struct { @@ -54,51 +54,37 @@ func NewProtocolClient(cc grpc.ClientConnInterface) ProtocolClient { return &protocolClient{cc} } -func (c *protocolClient) Stream(ctx context.Context, opts ...grpc.CallOption) (Protocol_StreamClient, error) { - stream, err := c.cc.NewStream(ctx, &Protocol_ServiceDesc.Streams[0], Protocol_Stream_FullMethodName, opts...) +func (c *protocolClient) Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Envelope, Envelope], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Protocol_ServiceDesc.Streams[0], Protocol_Stream_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &protocolStreamClient{stream} + x := &grpc.GenericClientStream[Envelope, Envelope]{ClientStream: stream} return x, nil } -type Protocol_StreamClient interface { - Send(*Envelope) error - Recv() (*Envelope, error) - grpc.ClientStream -} - -type protocolStreamClient struct { - grpc.ClientStream -} - -func (x *protocolStreamClient) Send(m *Envelope) error { - return x.ClientStream.SendMsg(m) -} - -func (x *protocolStreamClient) Recv() (*Envelope, error) { - m := new(Envelope) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Protocol_StreamClient = grpc.BidiStreamingClient[Envelope, Envelope] // ProtocolServer is the server API for Protocol service. // All implementations should embed UnimplementedProtocolServer -// for forward compatibility +// for forward compatibility. type ProtocolServer interface { - Stream(Protocol_StreamServer) error + Stream(grpc.BidiStreamingServer[Envelope, Envelope]) error } -// UnimplementedProtocolServer should be embedded to have forward compatible implementations. -type UnimplementedProtocolServer struct { -} +// UnimplementedProtocolServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedProtocolServer struct{} -func (UnimplementedProtocolServer) Stream(Protocol_StreamServer) error { +func (UnimplementedProtocolServer) Stream(grpc.BidiStreamingServer[Envelope, Envelope]) error { return status.Errorf(codes.Unimplemented, "method Stream not implemented") } +func (UnimplementedProtocolServer) testEmbeddedByValue() {} // UnsafeProtocolServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ProtocolServer will @@ -108,34 +94,22 @@ type UnsafeProtocolServer interface { } func RegisterProtocolServer(s grpc.ServiceRegistrar, srv ProtocolServer) { + // If the following call pancis, it indicates UnimplementedProtocolServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&Protocol_ServiceDesc, srv) } func _Protocol_Stream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(ProtocolServer).Stream(&protocolStreamServer{stream}) -} - -type Protocol_StreamServer interface { - Send(*Envelope) error - Recv() (*Envelope, error) - grpc.ServerStream -} - -type protocolStreamServer struct { - grpc.ServerStream + return srv.(ProtocolServer).Stream(&grpc.GenericServerStream[Envelope, Envelope]{ServerStream: stream}) } -func (x *protocolStreamServer) Send(m *Envelope) error { - return x.ServerStream.SendMsg(m) -} - -func (x *protocolStreamServer) Recv() (*Envelope, error) { - m := new(Envelope) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Protocol_StreamServer = grpc.BidiStreamingServer[Envelope, Envelope] // Protocol_ServiceDesc is the grpc.ServiceDesc for Protocol service. // It's only intended for direct use with grpc.RegisterService, From 8e4285fc7f317f0c8b6d22687dc1b5594186f9e0 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:33:59 +0200 Subject: [PATCH 07/72] Tools: upgrade oapi-codegen (#3435) --- api/generated.go | 2 +- auth/api/auth/v1/client/generated.go | 2 +- auth/api/auth/v1/generated.go | 2 +- auth/api/iam/generated.go | 2 +- auth/services/selfsigned/web/generated.go | 2 +- crypto/api/v1/generated.go | 2 +- crypto/storage/external/generated.go | 2 +- didman/api/v1/generated.go | 2 +- discovery/api/server/generated.go | 2 +- discovery/api/v1/generated.go | 2 +- e2e-tests/browser/client/iam/generated.go | 2 +- makefile | 2 +- network/api/v1/generated.go | 2 +- vcr/api/openid4vci/v0/generated.go | 2 +- vcr/api/vcr/v2/generated.go | 2 +- vdr/api/v1/generated.go | 2 +- vdr/api/v2/generated.go | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/api/generated.go b/api/generated.go index 223ba5662..34eabac44 100644 --- a/api/generated.go +++ b/api/generated.go @@ -1,6 +1,6 @@ // Package ssiTypes provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package ssiTypes import ( diff --git a/auth/api/auth/v1/client/generated.go b/auth/api/auth/v1/client/generated.go index 37129ee5f..ea4d4a745 100644 --- a/auth/api/auth/v1/client/generated.go +++ b/auth/api/auth/v1/client/generated.go @@ -1,6 +1,6 @@ // Package client provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package client import ( diff --git a/auth/api/auth/v1/generated.go b/auth/api/auth/v1/generated.go index 6b0cd5087..df2b8cefb 100644 --- a/auth/api/auth/v1/generated.go +++ b/auth/api/auth/v1/generated.go @@ -1,6 +1,6 @@ // Package v1 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v1 import ( diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index 8ab724b9e..69e55f6c0 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -1,6 +1,6 @@ // Package iam provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package iam import ( diff --git a/auth/services/selfsigned/web/generated.go b/auth/services/selfsigned/web/generated.go index 51648b182..143503944 100644 --- a/auth/services/selfsigned/web/generated.go +++ b/auth/services/selfsigned/web/generated.go @@ -1,6 +1,6 @@ // Package web provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package web import ( diff --git a/crypto/api/v1/generated.go b/crypto/api/v1/generated.go index e4fe5726d..e14065de5 100644 --- a/crypto/api/v1/generated.go +++ b/crypto/api/v1/generated.go @@ -1,6 +1,6 @@ // Package v1 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v1 import ( diff --git a/crypto/storage/external/generated.go b/crypto/storage/external/generated.go index 500120642..30a6c6600 100644 --- a/crypto/storage/external/generated.go +++ b/crypto/storage/external/generated.go @@ -1,6 +1,6 @@ // Package external provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package external import ( diff --git a/didman/api/v1/generated.go b/didman/api/v1/generated.go index 18f19f4cd..5d84dfb66 100644 --- a/didman/api/v1/generated.go +++ b/didman/api/v1/generated.go @@ -1,6 +1,6 @@ // Package v1 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v1 import ( diff --git a/discovery/api/server/generated.go b/discovery/api/server/generated.go index f60cf260a..48c0e0baa 100644 --- a/discovery/api/server/generated.go +++ b/discovery/api/server/generated.go @@ -1,6 +1,6 @@ // Package server provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package server import ( diff --git a/discovery/api/v1/generated.go b/discovery/api/v1/generated.go index 5beca050f..11e76317d 100644 --- a/discovery/api/v1/generated.go +++ b/discovery/api/v1/generated.go @@ -1,6 +1,6 @@ // Package v1 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v1 import ( diff --git a/e2e-tests/browser/client/iam/generated.go b/e2e-tests/browser/client/iam/generated.go index 3d8ca3487..a4fffed9f 100644 --- a/e2e-tests/browser/client/iam/generated.go +++ b/e2e-tests/browser/client/iam/generated.go @@ -1,6 +1,6 @@ // Package iam provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package iam import ( diff --git a/makefile b/makefile index eae9c441f..5e81448a6 100644 --- a/makefile +++ b/makefile @@ -3,7 +3,7 @@ run-generators: gen-mocks gen-api gen-protobuf install-tools: - go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.3.0 + go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.4.1 go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2 go install go.uber.org/mock/mockgen@v0.4.0 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 diff --git a/network/api/v1/generated.go b/network/api/v1/generated.go index 24ab9dcd7..6b13243e3 100644 --- a/network/api/v1/generated.go +++ b/network/api/v1/generated.go @@ -1,6 +1,6 @@ // Package v1 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v1 import ( diff --git a/vcr/api/openid4vci/v0/generated.go b/vcr/api/openid4vci/v0/generated.go index d6c6a149b..19426c73b 100644 --- a/vcr/api/openid4vci/v0/generated.go +++ b/vcr/api/openid4vci/v0/generated.go @@ -1,6 +1,6 @@ // Package v0 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v0 import ( diff --git a/vcr/api/vcr/v2/generated.go b/vcr/api/vcr/v2/generated.go index 5ecdc7882..19197b89f 100644 --- a/vcr/api/vcr/v2/generated.go +++ b/vcr/api/vcr/v2/generated.go @@ -1,6 +1,6 @@ // Package v2 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v2 import ( diff --git a/vdr/api/v1/generated.go b/vdr/api/v1/generated.go index 69fb5b251..73a244f36 100644 --- a/vdr/api/v1/generated.go +++ b/vdr/api/v1/generated.go @@ -1,6 +1,6 @@ // Package v1 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v1 import ( diff --git a/vdr/api/v2/generated.go b/vdr/api/v2/generated.go index 81ac871c4..8e05597ab 100644 --- a/vdr/api/v2/generated.go +++ b/vdr/api/v2/generated.go @@ -1,6 +1,6 @@ // Package v2 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package v2 import ( From 912f7710bad231e7e48fe796655764259eadf830 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:36:51 +0200 Subject: [PATCH 08/72] Tools: golangci lint (#3434) * Tools: upgrade golangci-lint + fixes --- auth/api/auth/v1/api.go | 8 ++++---- auth/api/iam/dpop.go | 4 ++-- auth/api/iam/jar.go | 2 +- auth/client/iam/openid4vp.go | 10 ---------- crypto/storage/vault/vault.go | 1 - discovery/client_test.go | 6 ++---- discovery/store.go | 11 ----------- discovery/store_test.go | 16 ++++++++++++---- http/user/session.go | 6 +++--- http/user/test.go | 2 +- makefile | 2 +- network/dag/parser.go | 2 +- policy/local.go | 3 +-- vcr/api/vcr/v2/api.go | 2 +- vdr/didsubject/manager.go | 3 +++ vdr/vdr.go | 3 +-- 16 files changed, 33 insertions(+), 48 deletions(-) diff --git a/auth/api/auth/v1/api.go b/auth/api/auth/v1/api.go index a608669f1..0d79c9d4a 100644 --- a/auth/api/auth/v1/api.go +++ b/auth/api/auth/v1/api.go @@ -271,7 +271,7 @@ func (w Wrapper) CreateJwtGrant(ctx context.Context, request CreateJwtGrantReque response, err := w.Auth.RelyingParty().CreateJwtGrant(ctx, req) if err != nil { - return nil, core.InvalidInputError(err.Error()) + return nil, core.InvalidInputError("%w", err) } return CreateJwtGrant200JSONResponse{BearerToken: response.BearerToken, AuthorizationServerEndpoint: response.AuthorizationServerEndpoint}, nil @@ -289,7 +289,7 @@ func (w Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo jwtGrant, err := w.Auth.RelyingParty().CreateJwtGrant(ctx, req) if err != nil { - return nil, core.InvalidInputError(err.Error()) + return nil, core.InvalidInputError("%w", err) } authServerEndpoint, err := url.Parse(jwtGrant.AuthorizationServerEndpoint) @@ -299,7 +299,7 @@ func (w Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo accessTokenResult, err := w.Auth.RelyingParty().RequestRFC003AccessToken(ctx, jwtGrant.BearerToken, *authServerEndpoint) if err != nil { - return nil, core.Error(http.StatusServiceUnavailable, err.Error()) + return nil, core.Error(http.StatusServiceUnavailable, "%w", err) } return RequestAccessToken200JSONResponse(*accessTokenResult), nil } @@ -401,7 +401,7 @@ func (w Wrapper) IntrospectAccessToken(ctx context.Context, request IntrospectAc introspectionResponse.AssuranceLevel = &level } - if claims.Credentials != nil && len(claims.Credentials) > 0 { + if len(claims.Credentials) > 0 { introspectionResponse.Vcs = &claims.Credentials var resolvedVCs []VerifiableCredential diff --git a/auth/api/iam/dpop.go b/auth/api/iam/dpop.go index 1e8e7ef12..6de6e379b 100644 --- a/auth/api/iam/dpop.go +++ b/auth/api/iam/dpop.go @@ -49,7 +49,7 @@ func (r Wrapper) CreateDPoPProof(ctx context.Context, request CreateDPoPProofReq // create new DPoP header httpRequest, err := http.NewRequest(request.Body.Htm, request.Body.Htu, nil) if err != nil { - return nil, core.InvalidInputError(err.Error()) + return nil, core.InvalidInputError("%w", err) } token := dpop.New(*httpRequest) token.GenerateProof(request.Body.Token) @@ -58,7 +58,7 @@ func (r Wrapper) CreateDPoPProof(ctx context.Context, request CreateDPoPProofReq // unescape manually here kid, err := url.PathUnescape(request.Kid) if err != nil { - return nil, core.InvalidInputError(err.Error()) + return nil, core.InvalidInputError("%w", err) } dpop, err := r.jwtSigner.SignDPoP(ctx, *token, kid) diff --git a/auth/api/iam/jar.go b/auth/api/iam/jar.go index 8c98ebeb9..8660a86b2 100644 --- a/auth/api/iam/jar.go +++ b/auth/api/iam/jar.go @@ -196,7 +196,7 @@ func compareThumbprint(configurationKey jwk.Key, publicKey crypto.PublicKey) err if err != nil { return err } - if bytes.Compare(thumbprintLeft, thumbprintRight) != 0 { + if !bytes.Equal(thumbprintLeft, thumbprintRight) { return errors.New("key thumbprints do not match") } return nil diff --git a/auth/client/iam/openid4vp.go b/auth/client/iam/openid4vp.go index 5fc02e084..0f9e37060 100644 --- a/auth/client/iam/openid4vp.go +++ b/auth/client/iam/openid4vp.go @@ -364,16 +364,6 @@ func (c *OpenID4VPClient) VerifiableCredentials(ctx context.Context, credentialE return rsp, nil } -func (c *OpenID4VPClient) walletWithExtraCredentials(ctx context.Context, subject did.DID, credentials []vc.VerifiableCredential) (holder.Wallet, error) { - walletCredentials, err := c.wallet.List(ctx, subject) - if err != nil { - return nil, err - } - return holder.NewMemoryWallet(c.ldDocumentLoader, c.keyResolver, c.jwtSigner, map[did.DID][]vc.VerifiableCredential{ - subject: append(walletCredentials, credentials...), - }), nil -} - func (c *OpenID4VPClient) dpop(ctx context.Context, requester did.DID, request http.Request) (string, string, error) { // find the key to sign the DPoP token with keyID, _, err := c.keyResolver.ResolveKey(requester, nil, resolver.AssertionMethod) diff --git a/crypto/storage/vault/vault.go b/crypto/storage/vault/vault.go index 4e1950951..62555de58 100644 --- a/crypto/storage/vault/vault.go +++ b/crypto/storage/vault/vault.go @@ -34,7 +34,6 @@ import ( const privateKeyPathName = "nuts-private-keys" const defaultPathPrefix = "kv" -const keyName = "key" // StorageType is the name of this storage type, used in health check reports and configuration. const StorageType = "vaultkv" diff --git a/discovery/client_test.go b/discovery/client_test.go index 511bf2096..76e147d1a 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -370,8 +370,7 @@ func Test_defaultClientRegistrationManager_refresh(t *testing.T) { assert.EqualError(t, err, errStr) // check for presentationRefreshError - refreshError, err := store.getPresentationRefreshError(testServiceID, bobSubject) - require.NoError(t, err) + refreshError := getPresentationRefreshError(t, store.db, testServiceID, bobSubject) assert.Contains(t, refreshError.Error, errStr) }) t.Run("deactivate unknown subject", func(t *testing.T) { @@ -431,8 +430,7 @@ func Test_defaultClientRegistrationManager_refresh(t *testing.T) { require.NoError(t, err) // check for presentationRefreshError - refreshError, err := store.getPresentationRefreshError(testServiceID, aliceSubject) - require.NoError(t, err) + refreshError := getPresentationRefreshError(t, store.db, testServiceID, aliceSubject) assert.Nil(t, refreshError) }) } diff --git a/discovery/store.go b/discovery/store.go index dc1b4a1c3..87bb94089 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -384,17 +384,6 @@ func (s *sqlStore) getSubjectsToBeRefreshed(now time.Time) ([]refreshCandidate, return result, nil } -func (s *sqlStore) getPresentationRefreshError(serviceID string, subjectID string) (*presentationRefreshError, error) { - var row presentationRefreshError - if err := s.db.Find(&row, "service_id = ? AND subject_id = ?", serviceID, subjectID).Error; err != nil { - return nil, err - } - if row.LastOccurrence == 0 { - return nil, nil - } - return &row, nil -} - func (s *sqlStore) setPresentationRefreshError(serviceID string, subjectID string, refreshErr error) error { return s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Delete(&presentationRefreshError{}, "service_id = ? AND subject_id = ?", serviceID, subjectID).Error; err != nil { diff --git a/discovery/store_test.go b/discovery/store_test.go index 0e6dae97c..d675905a6 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -305,9 +305,8 @@ func Test_sqlStore_setPresentationRefreshError(t *testing.T) { require.NoError(t, c.setPresentationRefreshError(testServiceID, aliceSubject, assert.AnError)) // Check if the error is stored - refreshError, err := c.getPresentationRefreshError(testServiceID, aliceSubject) + refreshError := getPresentationRefreshError(t, c.db, testServiceID, aliceSubject) - require.NoError(t, err) assert.Equal(t, refreshError.Error, assert.AnError.Error()) assert.True(t, refreshError.LastOccurrence > int(time.Now().Add(-1*time.Second).Unix())) }) @@ -317,9 +316,8 @@ func Test_sqlStore_setPresentationRefreshError(t *testing.T) { require.NoError(t, c.setPresentationRefreshError(testServiceID, aliceSubject, assert.AnError)) require.NoError(t, c.setPresentationRefreshError(testServiceID, aliceSubject, nil)) - refreshError, err := c.getPresentationRefreshError(testServiceID, aliceSubject) + refreshError := getPresentationRefreshError(t, c.db, testServiceID, aliceSubject) - require.NoError(t, err) assert.Nil(t, refreshError) }) } @@ -370,3 +368,13 @@ func resetStore(t *testing.T, db *gorm.DB) { require.NoError(t, db.Exec("DELETE FROM "+tableName).Error) } } + +func getPresentationRefreshError(t *testing.T, db *gorm.DB, serviceID string, subjectID string) *presentationRefreshError { + var row presentationRefreshError + err := db.Find(&row, "service_id = ? AND subject_id = ?", serviceID, subjectID).Error + require.NoError(t, err) + if row.LastOccurrence == 0 { + return nil + } + return &row +} diff --git a/http/user/session.go b/http/user/session.go index d000a43f6..0830762ca 100644 --- a/http/user/session.go +++ b/http/user/session.go @@ -36,7 +36,7 @@ import ( "time" ) -var userSessionContextKey = struct{}{} +type userSessionContextKey = struct{} // userSessionCookieName is the name of the cookie used to store the user session. // It uses the __Secure prefix, that instructs the user agent to treat it as a secure cookie: @@ -94,7 +94,7 @@ func (u SessionMiddleware) Handle(next echo.HandlerFunc) echo.HandlerFunc { return u.Store.Put(sessionID, sessionData) } // Session data is put in request context for access by API handlers - echoCtx.SetRequest(echoCtx.Request().WithContext(context.WithValue(echoCtx.Request().Context(), userSessionContextKey, sessionData))) + echoCtx.SetRequest(echoCtx.Request().WithContext(context.WithValue(echoCtx.Request().Context(), userSessionContextKey{}, sessionData))) return next(echoCtx) } @@ -166,7 +166,7 @@ func (u SessionMiddleware) createUserSessionCookie(sessionID string, path string // GetSession retrieves the user session from the request context. // If the user session is not found, an error is returned. func GetSession(ctx context.Context) (*Session, error) { - result, ok := ctx.Value(userSessionContextKey).(*Session) + result, ok := ctx.Value(userSessionContextKey{}).(*Session) if !ok { return nil, errors.New("no user session found") } diff --git a/http/user/test.go b/http/user/test.go index b7b3e9685..39c13ec4c 100644 --- a/http/user/test.go +++ b/http/user/test.go @@ -28,5 +28,5 @@ func CreateTestSession(ctx context.Context, subjectID string) (context.Context, session.Save = func() error { return nil } - return context.WithValue(ctx, userSessionContextKey, session), session + return context.WithValue(ctx, userSessionContextKey{}, session), session } diff --git a/makefile b/makefile index 5e81448a6..a9d8fed06 100644 --- a/makefile +++ b/makefile @@ -7,7 +7,7 @@ install-tools: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2 go install go.uber.org/mock/mockgen@v0.4.0 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0 gen-mocks: mockgen -destination=auth/mock.go -package=auth -source=auth/interface.go diff --git a/network/dag/parser.go b/network/dag/parser.go index 90d530008..e1be15758 100644 --- a/network/dag/parser.go +++ b/network/dag/parser.go @@ -97,7 +97,7 @@ func parsePayload(transaction *transaction, _ jws.Headers, message *jws.Message) func parseContentType(transaction *transaction, headers jws.Headers, _ *jws.Message) error { contentType := headers.ContentType() if !ValidatePayloadType(contentType) { - return transactionValidationError(errInvalidPayloadType.Error()) + return transactionValidationError("%w", errInvalidPayloadType) } transaction.payloadType = contentType return nil diff --git a/policy/local.go b/policy/local.go index 0b88facf5..eb18c7399 100644 --- a/policy/local.go +++ b/policy/local.go @@ -41,8 +41,7 @@ func New() *LocalPDP { // It loads a file with the mapping from oauth scope to PEX Policy. // It allows access when the requester can present a submission according to the Presentation Definition. type LocalPDP struct { - backend PDPBackend - config Config + config Config // mapping holds the oauth scope to PEX Policy mapping mapping map[string]validatingWalletOwnerMapping } diff --git a/vcr/api/vcr/v2/api.go b/vcr/api/vcr/v2/api.go index 10c9275a7..1105838ea 100644 --- a/vcr/api/vcr/v2/api.go +++ b/vcr/api/vcr/v2/api.go @@ -429,7 +429,7 @@ func (w *Wrapper) LoadVC(ctx context.Context, request LoadVCRequestObject) (Load // validate credential if err = w.VCR.Verifier().Verify(*request.Body, true, true, nil); err != nil { if errors.Is(err, verifier.VerificationError{}) { - return nil, core.InvalidInputError(err.Error()) + return nil, core.InvalidInputError("%w", err) } return nil, err } diff --git a/vdr/didsubject/manager.go b/vdr/didsubject/manager.go index 992cea064..a74825a00 100644 --- a/vdr/didsubject/manager.go +++ b/vdr/didsubject/manager.go @@ -733,6 +733,9 @@ func (r *SqlManager) MigrateAddWebToNuts(ctx context.Context, id did.DID) error // check if subject has a did:web subjectDIDs, err := r.ListDIDs(ctx, subject) + if err != nil { + return err + } for _, subjectDID := range subjectDIDs { if subjectDID.Method == "web" { // already has a did:web diff --git a/vdr/vdr.go b/vdr/vdr.go index 1924a369c..13f23fcca 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -160,7 +160,6 @@ func (r *Module) Configure(config core.ServerConfig) error { r.networkAmbassador = didnuts.NewAmbassador(r.network, r.store, r.eventManager) db := r.storageInstance.GetSQLDatabase() - methodManagers := make(map[string]didsubject.MethodManager) r.didResolver.(*resolver.DIDResolverRouter).Register(didjwk.MethodName, didjwk.NewResolver()) r.didResolver.(*resolver.DIDResolverRouter).Register(didkey.MethodName, didkey.NewResolver()) @@ -175,7 +174,7 @@ func (r *Module) Configure(config core.ServerConfig) error { } // Methods we can produce from the Nuts node - methodManagers = map[string]didsubject.MethodManager{} + methodManagers := map[string]didsubject.MethodManager{} // did:nuts nutsManager := didnuts.NewManager(r.keyStore, r.network, r.store, r.didResolver, db) From 6ff1537935476b6ea5ff93b9645c329f9e49beb7 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:15:26 +0200 Subject: [PATCH 09/72] add migration notes to docs (#3432) * add migration notes to docs * move migration documentation * fix config name --- docs/index.rst | 1 + docs/pages/deployment/migration.rst | 39 +++++++++++++++++++ .../integrating/version-incompatibilities.rst | 13 +------ docs/pages/release_notes.rst | 6 ++- 4 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 docs/pages/deployment/migration.rst diff --git a/docs/index.rst b/docs/index.rst index 9ce5996a7..041c5ca17 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,6 +23,7 @@ Nuts documentation pages/deployment/domain.rst pages/deployment/configuration.rst + pages/deployment/migration.rst pages/deployment/recommended-deployment.rst pages/deployment/docker.rst pages/deployment/storage.rst diff --git a/docs/pages/deployment/migration.rst b/docs/pages/deployment/migration.rst new file mode 100644 index 000000000..e093cbf17 --- /dev/null +++ b/docs/pages/deployment/migration.rst @@ -0,0 +1,39 @@ +.. _nuts-node-migration: + +Migrating from v5 to v6 +************************ + +Nuts node v6 runs several migrations on startup for DID documents that are managed by the node, namely: + +1. Remove controllers and add self-control to ``did:nuts`` documents, +2. Import ``did:nuts`` documents into the new SQL database under a ``subject`` with the same name, and +3. Add a ``did:web`` document with the same services to the same ``subject``. + +**Migration: convert did:nuts to self-control** +Requires ``vdr.didmethods`` to contain ``nuts``. + +Previously, DID documents could either by under self-control or under control of another DID as was recommended for vendor and care organisation, respectively. +In the new situation a user manages ``subject``s, and the node manages all DIDs under the ``subject``. +To reduce complexity and allow future adoption of other did methods, all documents will be under self-control from v6. + +**Migration: convert did:nuts to subject** +Requires ``vdr.didmethods`` to contain ``nuts``. + +All owned ``did:nuts`` DID documents will be migrated to the new SQL storage. +This migration includes all historic document updates as published upto a potential deactivation of the document. +For DIDs with a document conflict this is different than the resolved version of the document, which contains a merge of all conflicting document updates. +To prevent the state of the resolver and the SQL storage to be in conflict, all DID document conflicts must be resolved before upgrading to v6. +See ``/status/diagnostics`` if you own any DIDs with a document conflict. If so, use ``/internal/vdr/v1/did/conflicted`` to find the DIDs with a conflict. + +.. note:: + + The document migration will run on every restart of the node, meaning that any updates made using the VDR V1 API will be migrated on the next restart. + However, any changes made via the V1 API wil NOT propagate to other DID documents under the same ``subject``, so you MUST set ``vdr.didmethods = ["nuts"]`` to use the V1 API. + +**Migration: add did:web to subjects** +Requires ``vdr.didmethods`` to contain ``web`` and ``nuts`` (default). + +This migration adds a new ``did:web`` DID Document to owned subjects that do not already have one. +All services from the ``did:nuts`` DID Document are copied to the new document. +A new verification method is created for the document and added to all verification relationships except KeyAgreement. +This means did:web cannot be used for encryption (yet). diff --git a/docs/pages/integrating/version-incompatibilities.rst b/docs/pages/integrating/version-incompatibilities.rst index 659e39d82..139032f2a 100644 --- a/docs/pages/integrating/version-incompatibilities.rst +++ b/docs/pages/integrating/version-incompatibilities.rst @@ -11,17 +11,8 @@ There's also a config parameter that allows you to limit the DID methods in use. Not all combinations of API usage and DID methods are supported. There are basically two options. -1. Keep using the VDR V1 API (for now) and set ``vdr.did_methods`` to ``["nuts"]``. -2. Use the VDR V2 API and set ``vdr.did_methods`` to include other methods or leave blank for default setting. +1. Keep using the VDR V1 API (for now) and set ``vdr.didmethods`` to ``["nuts"]``. +2. Use the VDR V2 API and set ``vdr.didmethods`` to include other methods or leave blank for default setting. Do not use the VDR V1 and VDR V2 API at the same time. This will lead to unexpected behavior. Once you use the VDR V2 API, you cannot go back to the VDR V1 API. The VDR V1 API has also been marked as deprecated. - -Nodes running v6 with ``nuts`` configured as one of the ``vdr.did_methods`` will migrate all owned ``did:nuts`` DID documents to the new SQL storage. -This migration includes all historic document updates as published upto a potential deactivation of the document. -For DIDs with a document conflict this is different than the resolved version of the document, which contains a merge of all conflicting document updates. -To prevent the state of the resolver and the SQL storage to be in conflict, all DID document conflicts must be resolved before upgrading to v6. -See ``/status/diagnostics`` if you own any DIDs with a document conflict. If so, use ``/internal/vdr/v1/did/conflicted`` to find the DIDs with a conflict. - -The document migration will run on every restart of the node, meaning that any updates made using the VDR V1 API will be migrated on the next restart. -When switching from the VDR V1 API to the V2 API, the node must be restarted first to migrate any recent changes. diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index c32a39439..8bda03115 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -64,6 +64,7 @@ You no longer manage changes to DIDs but to Subjects. Each subject has multiple You're free to choose an ID for a Subject. This feature enables forwards compatibility with new DID methods. DID methods can be enabled and disabled via the ``vdr.didmethods`` config parameter. (Default: ``['web','nuts']``). Existing ``did:nuts`` documents will be migrated to self-controlled at startup and the DID will be added as SubjectID. +See :ref:`nuts-node-migrations` for more information. HTTP interface ============== @@ -95,7 +96,10 @@ The following features have been deprecated: Starting v6, the preferred way to support other key storage backends is to directly implement it in the Nuts node itself. This also reduces the complexity of a Nuts node deployment (one service less to configure and deploy). Users are recommended to switch to the built-in client of their key storage backend. -- VDR v1 API. +- Auth v1 API, replaced by Auth v2 +- DIDMan v1 API, to be removed +- Network v1 API, to be removed +- VDR v1 API, replaced by VDR v2 ************************ Hazelnut update (v5.4.11) From 8de5a4ea08dc64f9ecd70b1382de3cc0a5e9e491 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 2 Oct 2024 14:31:20 +0200 Subject: [PATCH 10/72] fix OAS for presentation definition and client in handling correct status code (#3438) --- auth/api/iam/generated.go | 17 ++++------------- auth/client/iam/client.go | 9 ++++++--- auth/client/iam/client_test.go | 22 ++++++++++++++++++++++ auth/client/iam/openid4vp_test.go | 13 ++++++++++--- docs/_static/auth/iam.partial.yaml | 13 ++++++++----- 5 files changed, 50 insertions(+), 24 deletions(-) diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index 69e55f6c0..60ea89cb5 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -1526,22 +1526,13 @@ func (response PresentationDefinition200JSONResponse) VisitPresentationDefinitio return json.NewEncoder(w).Encode(response) } -type PresentationDefinitiondefaultApplicationProblemPlusJSONResponse struct { - Body struct { - // Detail A human-readable explanation specific to this occurrence of the problem. - Detail string `json:"detail"` - - // Status HTTP statuscode - Status float32 `json:"status"` - - // Title A short, human-readable summary of the problem type. - Title string `json:"title"` - } +type PresentationDefinitiondefaultJSONResponse struct { + Body ErrorResponse StatusCode int } -func (response PresentationDefinitiondefaultApplicationProblemPlusJSONResponse) VisitPresentationDefinitionResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/problem+json") +func (response PresentationDefinitiondefaultJSONResponse) VisitPresentationDefinitionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(response.StatusCode) return json.NewEncoder(w).Encode(response.Body) diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go index 68a216ad4..bfcff021d 100644 --- a/auth/client/iam/client.go +++ b/auth/client/iam/client.go @@ -109,10 +109,10 @@ func (hb HTTPClient) PresentationDefinition(ctx context.Context, presentationDef var presentationDefinition pe.PresentationDefinition err = hb.doRequest(ctx, request, &presentationDefinition) if err != nil { - // a 404 (defined by scope) should result in a 400 for the client + // any OAuth error should be passed // any other error should result in a 502 Bad Gateway - if httpErr, ok := err.(core.HttpError); ok && httpErr.StatusCode == 404 { - return nil, errors.Join(ErrInvalidClientCall, err) + if oauthErr, ok := err.(oauth.OAuth2Error); ok { + return nil, oauthErr } return nil, errors.Join(ErrBadGateway, err) } @@ -399,6 +399,9 @@ func (hb HTTPClient) doRequest(ctx context.Context, request *http.Request, targe if ok, oauthErr := oauth.TestOAuthErrorCode(rse.ResponseBody, oauth.InvalidScope); ok { return oauthErr } + if ok, oauthErr := oauth.TestOAuthErrorCode(rse.ResponseBody, oauth.InvalidRequest); ok { + return oauthErr + } return httpErr } diff --git a/auth/client/iam/client_test.go b/auth/client/iam/client_test.go index bb205eff0..26d1a169b 100644 --- a/auth/client/iam/client_test.go +++ b/auth/client/iam/client_test.go @@ -109,6 +109,28 @@ func TestHTTPClient_PresentationDefinition(t *testing.T) { assert.Equal(t, definition, *response) require.NotNil(t, handler.Request) }) + t.Run("error - generic error results in 502", func(t *testing.T) { + handler := http2.Handler{StatusCode: http.StatusInternalServerError} + tlsServer, client := testServerAndClient(t, &handler) + pdUrl := test.MustParseURL(tlsServer.URL) + + _, err := client.PresentationDefinition(ctx, *pdUrl) + + require.Error(t, err) + assert.ErrorIs(t, err, ErrBadGateway) + }) + t.Run("error - oauth error", func(t *testing.T) { + handler := http2.Handler{StatusCode: http.StatusBadRequest, ResponseData: oauth.OAuth2Error{Code: oauth.InvalidRequest}} + tlsServer, client := testServerAndClient(t, &handler) + pdUrl := test.MustParseURL(tlsServer.URL) + + _, err := client.PresentationDefinition(ctx, *pdUrl) + + require.Error(t, err) + oauthErr, ok := err.(oauth.OAuth2Error) + require.True(t, ok) + assert.Equal(t, oauth.InvalidRequest, oauthErr.Code) + }) } func TestHTTPClient_AccessToken(t *testing.T) { diff --git a/auth/client/iam/openid4vp_test.go b/auth/client/iam/openid4vp_test.go index 13df76769..8c383ddb9 100644 --- a/auth/client/iam/openid4vp_test.go +++ b/auth/client/iam/openid4vp_test.go @@ -22,6 +22,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "github.com/nuts-foundation/nuts-node/http/client" test2 "github.com/nuts-foundation/nuts-node/test" @@ -356,13 +357,19 @@ func TestRelyingParty_RequestRFC021AccessToken(t *testing.T) { }) t.Run("error - failed to get presentation definition", func(t *testing.T) { ctx := createClientServerTestContext(t) - ctx.presentationDefinition = nil + ctx.presentationDefinition = func(writer http.ResponseWriter) { + writer.Header().Add("Content-Type", "application/json") + writer.WriteHeader(http.StatusBadRequest) + bytes, _ := json.Marshal(oauth.OAuth2Error{Code: oauth.InvalidScope}) + _, _ = writer.Write(bytes) + return + } _, err := ctx.client.RequestRFC021AccessToken(context.Background(), subjectClientID, subjectID, ctx.verifierURL.String(), scopes, false, nil) require.Error(t, err) - assert.ErrorIs(t, err, ErrInvalidClientCall) - assert.ErrorContains(t, err, "server returned HTTP 404 (expected: 200)") + assert.True(t, errors.As(err, &oauth.OAuth2Error{})) + assert.ErrorContains(t, err, string(oauth.InvalidScope)) }) t.Run("error - failed to get authorization server metadata", func(t *testing.T) { ctx := createClientServerTestContext(t) diff --git a/docs/_static/auth/iam.partial.yaml b/docs/_static/auth/iam.partial.yaml index 5c3756a58..e8520fab4 100644 --- a/docs/_static/auth/iam.partial.yaml +++ b/docs/_static/auth/iam.partial.yaml @@ -208,9 +208,8 @@ paths: summary: Used by relying parties to obtain a presentation definition for desired scopes as specified by Nuts RFC021. description: | The presentation definition (specified by https://identity.foundation/presentation-exchange/spec/v2.0.0/) is a JSON object that describes the desired verifiable credentials and presentation formats. - A presentation definition is matched against a wallet. If verifiable credentials matching the definition are found, - a presentation can created together with a presentation submission. - The API returns an array of definitions, one per scope/backend combination if applicable. + + It returns OAuth2 errors as specified by https://www.rfc-editor.org/rfc/rfc6749.html#section-5.2, specifically: invalid_request and invalid_scope. operationId: presentationDefinition tags: - oauth2 @@ -241,8 +240,12 @@ paths: application/json: schema: "$ref": "#/components/schemas/PresentationDefinition" - "default": - $ref: '../common/error_response.yaml' + default: + description: Error response + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /oauth2/{subjectID}/response: post: summary: Used by wallets to post the authorization response or error to. From 227b963b1a9fe6bb5f3f807a9462b745aeaa8e6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:31:44 +0200 Subject: [PATCH 11/72] Bump golang from 1.23.1-alpine to 1.23.2-alpine (#3440) Bumps golang from 1.23.1-alpine to 1.23.2-alpine. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8c868f07a..55ef226f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # golang alpine -FROM golang:1.23.1-alpine AS builder +FROM golang:1.23.2-alpine AS builder ARG TARGETARCH ARG TARGETOS From f4a2a14ba04baafcc0add742078426d23509c446 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 2 Oct 2024 14:33:48 +0200 Subject: [PATCH 12/72] fix status codes on did service APIs (#3429) --- docs/_static/vdr/v2.yaml | 5 ++++- vdr/api/v2/api.go | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/_static/vdr/v2.yaml b/docs/_static/vdr/v2.yaml index 9a254a444..7566e3a9a 100644 --- a/docs/_static/vdr/v2.yaml +++ b/docs/_static/vdr/v2.yaml @@ -298,7 +298,8 @@ paths: summary: Delete a specific service from the subject description: | Removes the service from all DID Documents in the subject. Matching is done on the fragment of the id. - No cascading will happen for references to the service. + No cascading will happen for references to the service. + Make sure to only URL encode the pound (#) as %23 in the serviceId. Do not encode the colons (:). error returns: * 400 - Returned in case of malformed subject or service ID @@ -316,10 +317,12 @@ paths: summary: Updates a service for the subject description: | It replaces the given service in all DID Documents of the subject by deleting the current service and adding the provided service with a newly generated ID. + Make sure to only URL encode the pound (#) as %23 in the serviceId. Do not encode the colons (:). error returns: * 400 - Returned in case of malformed subject or service ID * 404 - Corresponding subject or service could not be found + * 409 - Duplicate service type * 500 - An error occurred while processing the request tags: - Subject diff --git a/vdr/api/v2/api.go b/vdr/api/v2/api.go index d1e3c6950..7b330ce91 100644 --- a/vdr/api/v2/api.go +++ b/vdr/api/v2/api.go @@ -66,6 +66,8 @@ func (w *Wrapper) ResolveStatusCode(err error) int { didsubject.ErrKeyAgreementNotSupported: http.StatusBadRequest, didsubject.ErrSubjectValidation: http.StatusBadRequest, resolver.ErrDeactivated: http.StatusConflict, + did.ErrInvalidService: http.StatusBadRequest, + resolver.ErrDuplicateService: http.StatusConflict, }) } From f8ebb6cd3f768d3e4f5c909a35e123493301c208 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 2 Oct 2024 17:19:50 +0200 Subject: [PATCH 13/72] update koanf major (#3441) * update koanf major * fix broken test --------- Co-authored-by: Gerard Snaauw --- core/client_config.go | 2 +- core/client_config_test.go | 2 +- core/config.go | 2 +- core/config_test.go | 2 +- core/server_config.go | 2 +- .../rfc019_selfsigned/config/node/nuts.yaml | 2 +- .../browser/rfc019_selfsigned/run-test.sh | 3 +- go.mod | 15 +- go.sum | 272 ++---------------- main_test.go | 2 +- 10 files changed, 38 insertions(+), 266 deletions(-) diff --git a/core/client_config.go b/core/client_config.go index 02bf96444..5e44d433a 100644 --- a/core/client_config.go +++ b/core/client_config.go @@ -21,13 +21,13 @@ package core import ( "fmt" + "github.com/knadh/koanf/v2" "github.com/spf13/cobra" "os" "path" "strings" "time" - "github.com/knadh/koanf" "github.com/spf13/pflag" ) diff --git a/core/client_config_test.go b/core/client_config_test.go index 03c771381..184c15014 100644 --- a/core/client_config_test.go +++ b/core/client_config_test.go @@ -20,7 +20,7 @@ package core import ( - "github.com/knadh/koanf" + "github.com/knadh/koanf/v2" "github.com/nuts-foundation/nuts-node/test/io" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" diff --git a/core/config.go b/core/config.go index 0d9092085..b484b9719 100644 --- a/core/config.go +++ b/core/config.go @@ -22,11 +22,11 @@ package core import ( "errors" "fmt" - "github.com/knadh/koanf" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" "github.com/spf13/pflag" "os" "strings" diff --git a/core/config_test.go b/core/config_test.go index e106dc04e..e5009382e 100644 --- a/core/config_test.go +++ b/core/config_test.go @@ -20,7 +20,7 @@ package core import ( - "github.com/knadh/koanf" + "github.com/knadh/koanf/v2" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" diff --git a/core/server_config.go b/core/server_config.go index d2a9b0de6..a7f86eee5 100644 --- a/core/server_config.go +++ b/core/server_config.go @@ -25,9 +25,9 @@ import ( "crypto/x509" "errors" "fmt" - "github.com/knadh/koanf" "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" "github.com/sirupsen/logrus" "github.com/spf13/pflag" "net/url" diff --git a/e2e-tests/browser/rfc019_selfsigned/config/node/nuts.yaml b/e2e-tests/browser/rfc019_selfsigned/config/node/nuts.yaml index 2e5d52665..cb813d777 100644 --- a/e2e-tests/browser/rfc019_selfsigned/config/node/nuts.yaml +++ b/e2e-tests/browser/rfc019_selfsigned/config/node/nuts.yaml @@ -6,4 +6,4 @@ http: address: :8081 auth: contractvalidators: - - selfsigned + - employeeid diff --git a/e2e-tests/browser/rfc019_selfsigned/run-test.sh b/e2e-tests/browser/rfc019_selfsigned/run-test.sh index b054cc29b..8c8c3d86b 100755 --- a/e2e-tests/browser/rfc019_selfsigned/run-test.sh +++ b/e2e-tests/browser/rfc019_selfsigned/run-test.sh @@ -4,12 +4,11 @@ source ../../util.sh set -e # make script fail if any of the tests returns a non-zero exit code # Shut down existing containers -docker compose stop +docker compose down # Start new stack docker compose up --wait - go test -v --tags=e2e_tests . docker compose stop \ No newline at end of file diff --git a/go.mod b/go.mod index 3eca4aaa7..4249b9084 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,11 @@ require ( github.com/goodsign/monday v1.0.2 github.com/google/uuid v1.6.0 github.com/hashicorp/vault/api v1.15.0 - github.com/knadh/koanf v1.5.0 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/env v1.0.0 + github.com/knadh/koanf/providers/file v1.1.0 + github.com/knadh/koanf/providers/posflag v0.1.0 + github.com/knadh/koanf/providers/structs v0.1.0 github.com/labstack/echo/v4 v4.12.0 github.com/lestrrat-go/jwx/v2 v2.1.1 github.com/magiconair/properties v1.8.7 @@ -87,7 +91,7 @@ require ( github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/eknkc/basex v1.0.1 // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor v1.5.1 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-chi/chi/v5 v5.0.10 // indirect @@ -124,9 +128,9 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect @@ -199,3 +203,8 @@ require ( modernc.org/sqlite v1.32.0 // indirect rsc.io/qr v0.2.0 // indirect ) + +require ( + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/knadh/koanf/v2 v2.1.1 // indirect +) diff --git a/go.sum b/go.sum index 39f04019f..4da4589e6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,4 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= @@ -35,11 +34,6 @@ github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 h1:XIsQOSBJi/9Bexr+rjUpuYi0IkQ+YqNKKlE7Yt/sw9Q= github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97/go.mod h1:zTyVtYhYjcHpfCtqnCMxejgp0pEEwb/xJzhn05NrkJk= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexandrevicenzi/go-sse v1.6.0 h1:3KvOzpuY7UrbqZgAtOEmub9/V5ykr7Myudw+PA+H1Ik= github.com/alexandrevicenzi/go-sse v1.6.0/go.mod h1:jdrNAhMgVqP7OfcUuM8eJx0sOY17wc+girs5utpFZUU= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= @@ -47,27 +41,11 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGn github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= -github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= @@ -93,7 +71,6 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335 h1:bATMoZLH2QGct1kzDxfmeBUQI/QhQvB0mBrOTct+YlQ= @@ -104,9 +81,6 @@ github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3I github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -134,20 +108,16 @@ github.com/eknkc/basex v1.0.1/go.mod h1:k/F/exNEHFdbs3ZHuasoP2E7zeWwZblG84Y7Z59v github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= @@ -160,13 +130,6 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= @@ -180,10 +143,10 @@ github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukd github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -192,8 +155,6 @@ github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= @@ -216,29 +177,22 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/goodsign/monday v1.0.2 h1:k8kRMkCRVfCTWOU4dRfRgneQsWlB1+mJd3MxG0lGLzQ= github.com/goodsign/monday v1.0.2/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -247,14 +201,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -263,33 +212,18 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= -github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= -github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= @@ -297,34 +231,16 @@ github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3 github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= -github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= -github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -345,31 +261,26 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= -github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= +github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= +github.com/knadh/koanf/providers/file v1.1.0 h1:MTjA+gRrVl1zqgetEAIaXHqYje0XSosxSiMD4/7kz0o= +github.com/knadh/koanf/providers/file v1.1.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= +github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -403,19 +314,12 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= @@ -425,8 +329,6 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyex github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= @@ -435,27 +337,16 @@ github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1 github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -473,8 +364,6 @@ github.com/multiformats/go-multihash v0.0.11 h1:yEyBxwoR/7vBM5NfLVXRnpQNVLrMhpS6 github.com/multiformats/go-multihash v0.0.11/go.mod h1:LXRDJcYYY+9BjlsFe6i5LV7uekf0OoEJdnRmitUshxk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE= github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= github.com/nats-io/nats-server/v2 v2.10.21 h1:gfG6T06wBdI25XyY2IsauarOc2srWoFxxfsOKjrzoRA= @@ -489,7 +378,6 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= -github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b h1:80icUxWHwE1MrIOOEK5rxrtyKOgZeq5Iu1IjAEkggTY= github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b/go.mod h1:6YUioYirD6/8IahZkoS4Ypc8xbeJW76Xdk1QKcziNTM= github.com/nuts-foundation/go-did v0.14.0 h1:Y1tuQCC2xmDX1bdXQS9iquwzJgcT1zcJxbZkqC5Dfac= @@ -502,34 +390,24 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f h1:HlPa7RcxTCrva5izPfTEfvYecO7LTahgmMRD1Qp13xg= github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k= github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/pressly/goose/v3 v3.22.0 h1:wd/7kNiPTuNAztWun7iaB98DrhulbWPrzMAaw2DEZNw= @@ -538,27 +416,13 @@ github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750 h1:3RuYOQTlAr github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750/go.mod h1:QZI8hX8Ff2GfZ7UJuxyWw3nAGgt2s5+U4hxY6rmwQvs= github.com/privacybydesign/irmago v0.16.0 h1:PxIPRvpitxfJSocIRwIoYDSETarsopxtByZ5XZ3yzcE= github.com/privacybydesign/irmago v0.16.0/go.mod h1:++4PaFrN8MuRkunja/ID6cX+5KcATi7I0uVG+asI96c= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= @@ -567,22 +431,18 @@ github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= @@ -594,9 +454,6 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50 h1:vgWWQM2SnMoO9BiUZ2WFAYuYF6U0jNss9Vn/PZoi+tU= github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50/go.mod h1:W/QHK9G0i5yrmHvej5+hhoFMXTSZIWHGQRcpbGgqV9s= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -608,13 +465,10 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -652,21 +506,14 @@ github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJ github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -674,15 +521,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -699,35 +541,20 @@ golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCR golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -738,57 +565,24 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220519141025-dcacdad47464/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -813,10 +607,7 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -825,7 +616,6 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -834,39 +624,25 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -878,35 +654,24 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 h1:rz88vn1OH2B9kKorR+QCrcuw6WbizVwahU2Y9Q09xqU= gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3/go.mod h1:vJmfdx2L0+30M90zUd0GCjLV14Ip3ZgWR5+MV1qljOo= gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 h1:pSSZonNnrORBQXIm3kl6P9EQTNqVds9zszK/BXbOItg= gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3/go.mod h1:5u3BxKhx1TujE5j4Jj53c3uNTRUqOlxM5I5c4zDhEjA= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= @@ -951,4 +716,3 @@ rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= schneider.vip/problem v1.9.1 h1:HYdGPzbTHnNziF7cC4ftbn/eTrjSIXhKfricAMaLIMk= schneider.vip/problem v1.9.1/go.mod h1:6hLRfO1e1MQWdG23Kl5b3Yp5FSexE+YiGVqCkAp3HUQ= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/main_test.go b/main_test.go index 62d34e3a5..0a005594c 100644 --- a/main_test.go +++ b/main_test.go @@ -22,9 +22,9 @@ import ( "context" "errors" "fmt" - "github.com/knadh/koanf" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/structs" + "github.com/knadh/koanf/v2" "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/cmd" "github.com/nuts-foundation/nuts-node/core" From d888d2c99b79d9efc36828204897f7307e08c1b8 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:17:04 +0200 Subject: [PATCH 14/72] Move didmethods to config root (#3439) * Move didmethods to config root --- README.rst | 3 +- auth/api/auth/v1/api_test.go | 26 ++++++++----- auth/api/iam/api.go | 14 +++---- auth/api/iam/api_test.go | 7 +--- auth/api/iam/openid4vp.go | 2 +- auth/auth.go | 39 +++++++++++-------- auth/interface.go | 2 + auth/mock.go | 14 +++++++ cmd/root.go | 11 ++---- core/server_config.go | 4 ++ docs/pages/deployment/migration.rst | 8 ++-- docs/pages/deployment/server_options.rst | 3 +- .../integrating/version-incompatibilities.rst | 4 +- docs/pages/release_notes.rst | 2 +- .../config/nuts.yaml | 6 +-- e2e-tests/discovery/node-A/nuts.yaml | 4 +- e2e-tests/discovery/node-B/nuts.yaml | 6 +-- .../oauth-flow/openid4vp/node-A/nuts.yaml | 6 +-- .../oauth-flow/openid4vp/node-B/nuts.yaml | 6 +-- e2e-tests/oauth-flow/rfc002/node-A/nuts.yaml | 4 +- e2e-tests/oauth-flow/rfc002/node-B/nuts.yaml | 6 +-- .../statuslist2021/node-A/nuts.yaml | 4 +- .../statuslist2021/node-B/nuts.yaml | 6 +-- main_test.go | 7 +--- test/node/server.go | 2 +- vcr/test.go | 3 -- vdr/cmd/cmd.go | 13 ------- vdr/cmd/cmd_test.go | 4 -- vdr/config.go | 29 -------------- vdr/didsubject/mock.go | 14 +++++++ vdr/interface.go | 2 - vdr/legacy_integration_test.go | 2 +- vdr/mock.go | 14 ------- vdr/vdr.go | 35 +++++++---------- vdr/vdr_test.go | 14 ++----- 35 files changed, 129 insertions(+), 197 deletions(-) delete mode 100644 vdr/config.go diff --git a/README.rst b/README.rst index b62683271..249ca7862 100644 --- a/README.rst +++ b/README.rst @@ -173,6 +173,7 @@ The following options can be configured on the server: configfile ./config/nuts.yaml Nuts config file cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. datadir ./data Directory where the node stores its files. + didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used. internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. loggerformat text Log format (text, json) strictmode true When set, insecure settings are forbidden. @@ -216,8 +217,6 @@ The following options can be configured on the server: storage.session.redis.username Redis session database username. If set, it overrides the username in the connection URL. storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL'). - **VDR** - vdr.didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used. **policy** policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================ diff --git a/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go index 14e49d5d6..cb783e904 100644 --- a/auth/api/auth/v1/api_test.go +++ b/auth/api/auth/v1/api_test.go @@ -61,11 +61,12 @@ type TestContext struct { var _ pkg2.AuthenticationServices = &mockAuthClient{} type mockAuthClient struct { - ctrl *gomock.Controller - authzServer *oauth.MockAuthorizationServer - contractNotary *services.MockContractNotary - iamClient *iam.MockClient - relyingParty *oauth.MockRelyingParty + ctrl *gomock.Controller + authzServer *oauth.MockAuthorizationServer + contractNotary *services.MockContractNotary + iamClient *iam.MockClient + relyingParty *oauth.MockRelyingParty + supportedDIDMethods []string } func (m *mockAuthClient) AuthorizationEndpointEnabled() bool { @@ -92,6 +93,10 @@ func (m *mockAuthClient) PublicURL() *url.URL { return nil } +func (m *mockAuthClient) SupportedDIDMethods() []string { + return m.supportedDIDMethods +} + func createContext(t *testing.T) *TestContext { t.Helper() ctrl := gomock.NewController(t) @@ -102,11 +107,12 @@ func createContext(t *testing.T) *TestContext { iamClient := iam.NewMockClient(ctrl) authMock := &mockAuthClient{ - ctrl: ctrl, - contractNotary: contractNotary, - authzServer: authzServer, - relyingParty: relyingParty, - iamClient: iamClient, + ctrl: ctrl, + contractNotary: contractNotary, + authzServer: authzServer, + relyingParty: relyingParty, + iamClient: iamClient, + supportedDIDMethods: []string{"web", "nuts"}, } requestCtx := audit.TestContext() diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index f8d660bed..a0986f25d 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -53,7 +53,6 @@ import ( "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/pe" - "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/didsubject" "github.com/nuts-foundation/nuts-node/vdr/resolver" ) @@ -98,7 +97,6 @@ type Wrapper struct { storageEngine storage.Engine jsonldManager jsonld.JSONLD vcr vcr.VCR - vdr vdr.VDR jwtSigner nutsCrypto.JWTSigner keyResolver resolver.KeyResolver subjectManager didsubject.Manager @@ -106,7 +104,7 @@ type Wrapper struct { } func New( - authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInstance vdr.VDR, subjectManager didsubject.Manager, storageEngine storage.Engine, + authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, didKeyResolver resolver.DIDKeyResolver, subjectManager didsubject.Manager, storageEngine storage.Engine, policyBackend policy.PDPBackend, jwtSigner nutsCrypto.JWTSigner, jsonldManager jsonld.JSONLD) *Wrapper { templates := template.New("oauth2 templates") @@ -114,21 +112,19 @@ func New( if err != nil { panic(err) } - keyResolver := resolver.DIDKeyResolver{Resolver: vdrInstance.Resolver()} return &Wrapper{ auth: authInstance, policyBackend: policyBackend, storageEngine: storageEngine, vcr: vcrInstance, - vdr: vdrInstance, subjectManager: subjectManager, jsonldManager: jsonldManager, jwtSigner: jwtSigner, - keyResolver: keyResolver, + keyResolver: didKeyResolver, jar: jar{ auth: authInstance, jwtSigner: jwtSigner, - keyResolver: keyResolver, + keyResolver: didKeyResolver, }, } } @@ -603,7 +599,7 @@ func (r Wrapper) OAuthAuthorizationServerMetadata(_ context.Context, request OAu } func (r Wrapper) oauthAuthorizationServerMetadata(clientID url.URL) (*oauth.AuthorizationServerMetadata, error) { - md := authorizationServerMetadata(&clientID, r.vdr.SupportedMethods()) + md := authorizationServerMetadata(&clientID, r.auth.SupportedDIDMethods()) if !r.auth.AuthorizationEndpointEnabled() { md.AuthorizationEndpoint = "" } @@ -667,7 +663,7 @@ func (r Wrapper) OpenIDConfiguration(ctx context.Context, request OpenIDConfigur // this is a shortcoming of the openID federation vs OpenID4VP/DID worlds // issuer URL equals server baseURL + :/oauth2/:subject issuerURL := r.subjectToBaseURL(request.SubjectID) - configuration := openIDConfiguration(issuerURL, set, r.vdr.SupportedMethods()) + configuration := openIDConfiguration(issuerURL, set, r.auth.SupportedDIDMethods()) claims := make(map[string]interface{}) asJson, _ := json.Marshal(configuration) _ = json.Unmarshal(asJson, &claims) diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 8dbdb2012..32bc20475 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -1419,7 +1419,6 @@ func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled b vcIssuer := issuer.NewMockIssuer(ctrl) vcVerifier := verifier.NewMockVerifier(ctrl) iamClient := iam.NewMockClient(ctrl) - mockVDR := vdr.NewMockVDR(ctrl) mockDocumentOwner := didsubject.NewMockDocumentOwner(ctrl) subjectManager := didsubject.NewMockManager(ctrl) mockVCR := vcr.NewMockVCR(ctrl) @@ -1430,14 +1429,12 @@ func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled b authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes() authnServices.EXPECT().RelyingParty().Return(relyingPary).AnyTimes() + authnServices.EXPECT().SupportedDIDMethods().Return([]string{"web"}).AnyTimes() mockVCR.EXPECT().Issuer().Return(vcIssuer).AnyTimes() mockVCR.EXPECT().Verifier().Return(vcVerifier).AnyTimes() mockVCR.EXPECT().Wallet().Return(mockWallet).AnyTimes() authnServices.EXPECT().IAMClient().Return(iamClient).AnyTimes() authnServices.EXPECT().AuthorizationEndpointEnabled().Return(authEndpointEnabled).AnyTimes() - mockVDR.EXPECT().Resolver().Return(mockResolver).AnyTimes() - mockVDR.EXPECT().DocumentOwner().Return(mockDocumentOwner).AnyTimes() - mockVDR.EXPECT().SupportedMethods().Return([]string{"web"}).AnyTimes() subjectManager.EXPECT().ListDIDs(gomock.Any(), holderSubjectID).Return([]did.DID{holderDID}, nil).AnyTimes() subjectManager.EXPECT().ListDIDs(gomock.Any(), unknownSubjectID).Return(nil, didsubject.ErrSubjectNotFound).AnyTimes() @@ -1449,7 +1446,6 @@ func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled b client := &Wrapper{ auth: authnServices, - vdr: mockVDR, subjectManager: subjectManager, vcr: mockVCR, storageEngine: storageEngine, @@ -1466,7 +1462,6 @@ func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled b vcIssuer: vcIssuer, vcVerifier: vcVerifier, resolver: mockResolver, - vdr: mockVDR, documentOwner: mockDocumentOwner, subjectManager: subjectManager, iamClient: iamClient, diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 62e32a3c8..405a52e7b 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -384,7 +384,7 @@ func (r Wrapper) sendAndHandleDirectPost(ctx context.Context, subject string, vp // Dispatch a new HTTP request to the local OpenID4VP wallet's authorization endpoint that includes request parameters, // but with openid4vp: as scheme. // The context contains data from the previous request. Usage by the handler will probably result in incorrect behavior. - userWalletMetadata := authorizationServerMetadata(nil, r.vdr.SupportedMethods()) + userWalletMetadata := authorizationServerMetadata(nil, r.auth.SupportedDIDMethods()) response, err := r.handleAuthorizeRequest(ctx, subject, userWalletMetadata, *parsedRedirectURI) if err != nil { return nil, err diff --git a/auth/auth.go b/auth/auth.go index 4586b93c5..0c934d098 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -46,22 +46,23 @@ var _ AuthenticationServices = (*Auth)(nil) // Auth is the main struct of the Auth service type Auth struct { - config Config - jsonldManager jsonld.JSONLD - authzServer oauth.AuthorizationServer - relyingParty oauth.RelyingParty - contractNotary services.ContractNotary - serviceResolver didman.CompoundServiceResolver - keyStore crypto.KeyStore - vcr vcr.VCR - pkiProvider pki.Provider - shutdownFunc func() - vdrInstance vdr.VDR - publicURL *url.URL - strictMode bool - httpClientTimeout time.Duration - tlsConfig *tls.Config - subjectManager didsubject.Manager + config Config + jsonldManager jsonld.JSONLD + authzServer oauth.AuthorizationServer + relyingParty oauth.RelyingParty + contractNotary services.ContractNotary + serviceResolver didman.CompoundServiceResolver + keyStore crypto.KeyStore + vcr vcr.VCR + pkiProvider pki.Provider + shutdownFunc func() + vdrInstance vdr.VDR + publicURL *url.URL + strictMode bool + httpClientTimeout time.Duration + tlsConfig *tls.Config + subjectManager didsubject.Manager + supportedDIDMethods []string } // Name returns the name of the module. @@ -136,6 +137,8 @@ func (auth *Auth) Configure(config core.ServerConfig) error { return err } + auth.supportedDIDMethods = config.DIDMethods + auth.contractNotary = notary.NewNotary(notary.Config{ PublicURL: auth.publicURL.String(), IrmaConfigPath: path.Join(config.Datadir, "irma"), @@ -175,6 +178,10 @@ func (auth *Auth) Configure(config core.ServerConfig) error { return nil } +func (auth *Auth) SupportedDIDMethods() []string { + return auth.supportedDIDMethods +} + // Start starts the Auth engine (Noop) func (auth *Auth) Start() error { return nil diff --git a/auth/interface.go b/auth/interface.go index ee1cad0c6..bae296c0d 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -42,4 +42,6 @@ type AuthenticationServices interface { PublicURL() *url.URL // AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled. AuthorizationEndpointEnabled() bool + // SupportedDIDMethods list the DID methods configured for the nuts node in preferred order. + SupportedDIDMethods() []string } diff --git a/auth/mock.go b/auth/mock.go index a9beb62de..87c1fe580 100644 --- a/auth/mock.go +++ b/auth/mock.go @@ -125,3 +125,17 @@ func (mr *MockAuthenticationServicesMockRecorder) RelyingParty() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RelyingParty", reflect.TypeOf((*MockAuthenticationServices)(nil).RelyingParty)) } + +// SupportedDIDMethods mocks base method. +func (m *MockAuthenticationServices) SupportedDIDMethods() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupportedDIDMethods") + ret0, _ := ret[0].([]string) + return ret0 +} + +// SupportedDIDMethods indicates an expected call of SupportedDIDMethods. +func (mr *MockAuthenticationServicesMockRecorder) SupportedDIDMethods() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedDIDMethods", reflect.TypeOf((*MockAuthenticationServices)(nil).SupportedDIDMethods)) +} diff --git a/cmd/root.go b/cmd/root.go index 918595e90..ff1a64421 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -206,20 +206,18 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { policyInstance := policy.New() // Register HTTP routes + didKeyResolver := resolver.DIDKeyResolver{Resolver: vdrInstance.Resolver()} system.RegisterRoutes(&core.LandingPage{}) - system.RegisterRoutes(&cryptoAPI.Wrapper{C: cryptoInstance, K: resolver.DIDKeyResolver{Resolver: vdrInstance.Resolver()}}) + system.RegisterRoutes(&cryptoAPI.Wrapper{C: cryptoInstance, K: didKeyResolver}) system.RegisterRoutes(&networkAPI.Wrapper{Service: networkInstance}) system.RegisterRoutes(&vdrAPI.Wrapper{VDR: vdrInstance, SubjectManager: vdrInstance}) system.RegisterRoutes(&vdrAPIv2.Wrapper{VDR: vdrInstance, SubjectManager: vdrInstance}) system.RegisterRoutes(&vcrAPI.Wrapper{VCR: credentialInstance, ContextManager: jsonld, SubjectManager: vdrInstance}) - system.RegisterRoutes(&openid4vciAPI.Wrapper{ - VCR: credentialInstance, - VDR: vdrInstance, - }) + system.RegisterRoutes(&openid4vciAPI.Wrapper{VCR: credentialInstance, VDR: vdrInstance}) system.RegisterRoutes(statusEngine.(core.Routable)) system.RegisterRoutes(metricsEngine.(core.Routable)) system.RegisterRoutes(&authAPIv1.Wrapper{Auth: authInstance, CredentialResolver: credentialInstance}) - system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, vdrInstance, vdrInstance, storageInstance, policyInstance, cryptoInstance, jsonld)) + system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, didKeyResolver, vdrInstance, storageInstance, policyInstance, cryptoInstance, jsonld)) system.RegisterRoutes(&authMeansAPI.Wrapper{Auth: authInstance}) system.RegisterRoutes(&didmanAPI.Wrapper{Didman: didmanInstance}) system.RegisterRoutes(&discoveryAPI.Wrapper{Client: discoveryInstance}) @@ -334,7 +332,6 @@ func serverConfigFlags() *pflag.FlagSet { set.AddFlagSet(httpCmd.FlagSet()) set.AddFlagSet(storageCmd.FlagSet()) set.AddFlagSet(networkCmd.FlagSet()) - set.AddFlagSet(vdrCmd.FlagSet()) set.AddFlagSet(vcrCmd.FlagSet()) set.AddFlagSet(jsonld.FlagSet()) set.AddFlagSet(authCmd.FlagSet()) diff --git a/core/server_config.go b/core/server_config.go index a7f86eee5..b9cfc6bfc 100644 --- a/core/server_config.go +++ b/core/server_config.go @@ -56,6 +56,7 @@ var redactedConfigKeys = []string{ type ServerConfig struct { // URL contains the base URL for public-facing HTTP services. URL string `koanf:"url"` + DIDMethods []string `koanf:"didmethods"` Verbosity string `koanf:"verbosity"` LoggerFormat string `koanf:"loggerformat"` CPUProfile string `koanf:"cpuprofile"` @@ -156,6 +157,7 @@ func NewServerConfig() *ServerConfig { Strictmode: true, InternalRateLimiter: true, Datadir: "./data", + DIDMethods: []string{"web", "nuts"}, TLS: TLSConfig{ TrustStoreFile: "./config/ssl/truststore.pem", Offload: NoOffloading, @@ -253,6 +255,8 @@ func FlagSet() *pflag.FlagSet { flagSet.Bool("internalratelimiter", defaultCfg.InternalRateLimiter, "When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode.") flagSet.String("datadir", defaultCfg.Datadir, "Directory where the node stores its files.") flagSet.String("url", defaultCfg.URL, "Public facing URL of the server (required). Must be HTTPS when strictmode is set.") + flagSet.StringSlice("didmethods", defaultCfg.DIDMethods, "Comma-separated list of enabled DID methods (without did: prefix). "+ + "It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used.") flagSet.Duration("httpclient.timeout", defaultCfg.HTTPClient.Timeout, "Request time-out for HTTP clients, such as '10s'. Refer to Golang's 'time.Duration' syntax for a more elaborate description of the syntax.") flagSet.String("tls.certfile", defaultCfg.TLS.CertFile, "PEM file containing the certificate for the gRPC server (also used as client certificate). Required in strict mode.") flagSet.String("tls.certkeyfile", defaultCfg.TLS.CertKeyFile, "PEM file containing the private key of the gRPC server certificate. Required in strict mode.") diff --git a/docs/pages/deployment/migration.rst b/docs/pages/deployment/migration.rst index e093cbf17..a5f164886 100644 --- a/docs/pages/deployment/migration.rst +++ b/docs/pages/deployment/migration.rst @@ -10,14 +10,14 @@ Nuts node v6 runs several migrations on startup for DID documents that are manag 3. Add a ``did:web`` document with the same services to the same ``subject``. **Migration: convert did:nuts to self-control** -Requires ``vdr.didmethods`` to contain ``nuts``. +Requires ``didmethods`` to contain ``nuts``. Previously, DID documents could either by under self-control or under control of another DID as was recommended for vendor and care organisation, respectively. In the new situation a user manages ``subject``s, and the node manages all DIDs under the ``subject``. To reduce complexity and allow future adoption of other did methods, all documents will be under self-control from v6. **Migration: convert did:nuts to subject** -Requires ``vdr.didmethods`` to contain ``nuts``. +Requires ``didmethods`` to contain ``nuts``. All owned ``did:nuts`` DID documents will be migrated to the new SQL storage. This migration includes all historic document updates as published upto a potential deactivation of the document. @@ -28,10 +28,10 @@ See ``/status/diagnostics`` if you own any DIDs with a document conflict. If so, .. note:: The document migration will run on every restart of the node, meaning that any updates made using the VDR V1 API will be migrated on the next restart. - However, any changes made via the V1 API wil NOT propagate to other DID documents under the same ``subject``, so you MUST set ``vdr.didmethods = ["nuts"]`` to use the V1 API. + However, any changes made via the V1 API wil NOT propagate to other DID documents under the same ``subject``, so you MUST set ``didmethods = ["nuts"]`` to use the V1 API. **Migration: add did:web to subjects** -Requires ``vdr.didmethods`` to contain ``web`` and ``nuts`` (default). +Requires ``didmethods`` to contain ``web`` and ``nuts`` (default). This migration adds a new ``did:web`` DID Document to owned subjects that do not already have one. All services from the ``did:nuts`` DID Document are copied to the new document. diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 96c3669cf..8afc2d976 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -8,6 +8,7 @@ configfile ./config/nuts.yaml Nuts config file cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. datadir ./data Directory where the node stores its files. + didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used. internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. loggerformat text Log format (text, json) strictmode true When set, insecure settings are forbidden. @@ -51,8 +52,6 @@ storage.session.redis.username Redis session database username. If set, it overrides the username in the connection URL. storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL'). - **VDR** - vdr.didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used. **policy** policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. ======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================ diff --git a/docs/pages/integrating/version-incompatibilities.rst b/docs/pages/integrating/version-incompatibilities.rst index 139032f2a..6ab7371be 100644 --- a/docs/pages/integrating/version-incompatibilities.rst +++ b/docs/pages/integrating/version-incompatibilities.rst @@ -11,8 +11,8 @@ There's also a config parameter that allows you to limit the DID methods in use. Not all combinations of API usage and DID methods are supported. There are basically two options. -1. Keep using the VDR V1 API (for now) and set ``vdr.didmethods`` to ``["nuts"]``. -2. Use the VDR V2 API and set ``vdr.didmethods`` to include other methods or leave blank for default setting. +1. Keep using the VDR V1 API (for now) and set ``didmethods`` to ``["nuts"]``. +2. Use the VDR V2 API and set ``didmethods`` to include other methods or leave blank for default setting. Do not use the VDR V1 and VDR V2 API at the same time. This will lead to unexpected behavior. Once you use the VDR V2 API, you cannot go back to the VDR V1 API. The VDR V1 API has also been marked as deprecated. diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index 8bda03115..240e6d677 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -62,7 +62,7 @@ DID management You no longer manage changes to DIDs but to Subjects. Each subject has multiple DIDs, one for each enabled DID method. You're free to choose an ID for a Subject. This feature enables forwards compatibility with new DID methods. -DID methods can be enabled and disabled via the ``vdr.didmethods`` config parameter. (Default: ``['web','nuts']``). +DID methods can be enabled and disabled via the ``didmethods`` config parameter. (Default: ``['web','nuts']``). Existing ``did:nuts`` documents will be migrated to self-controlled at startup and the DID will be added as SubjectID. See :ref:`nuts-node-migrations` for more information. diff --git a/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml b/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml index eef6fe6a4..3f31e3dd6 100644 --- a/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml +++ b/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml @@ -1,4 +1,5 @@ url: https://nodeA +didmethods: ["web"] strictmode: false verbosity: debug http: @@ -11,7 +12,4 @@ auth: authorizationendpoint: enabled: true policy: - directory: /opt/nuts/policy -vdr: - didmethods: - - web \ No newline at end of file + directory: /opt/nuts/policy \ No newline at end of file diff --git a/e2e-tests/discovery/node-A/nuts.yaml b/e2e-tests/discovery/node-A/nuts.yaml index af2ad4acc..1bb1772d8 100644 --- a/e2e-tests/discovery/node-A/nuts.yaml +++ b/e2e-tests/discovery/node-A/nuts.yaml @@ -1,4 +1,5 @@ url: http://nodeA +didmethods: ["web"] verbosity: debug strictmode: false internalratelimiter: false @@ -20,6 +21,3 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem -vdr: - didmethods: - - web diff --git a/e2e-tests/discovery/node-B/nuts.yaml b/e2e-tests/discovery/node-B/nuts.yaml index b48a05b06..fb654b3a1 100644 --- a/e2e-tests/discovery/node-B/nuts.yaml +++ b/e2e-tests/discovery/node-B/nuts.yaml @@ -1,4 +1,5 @@ url: https://nodeB +didmethods: ["web"] verbosity: debug strictmode: false internalratelimiter: false @@ -19,7 +20,4 @@ auth: tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem - certkeyfile: /opt/nuts/certificate-and-key.pem -vdr: - didmethods: - - web \ No newline at end of file + certkeyfile: /opt/nuts/certificate-and-key.pem \ No newline at end of file diff --git a/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml index 2496034a3..e6e6a13d6 100644 --- a/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml +++ b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml @@ -1,4 +1,5 @@ url: https://nodeA +didmethods: ["web"] verbosity: debug strictmode: false internalratelimiter: false @@ -14,7 +15,4 @@ auth: authorizationendpoint: enabled: true policy: - directory: /opt/nuts/policies -vdr: - didmethods: - - web \ No newline at end of file + directory: /opt/nuts/policies \ No newline at end of file diff --git a/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml index b7d5d86c4..ae8598702 100644 --- a/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml +++ b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml @@ -1,4 +1,5 @@ url: https://nodeB +didmethods: ["web"] verbosity: debug strictmode: false internalratelimiter: false @@ -17,7 +18,4 @@ policy: tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem - certkeyfile: /opt/nuts/certificate-and-key.pem -vdr: - didmethods: - - web \ No newline at end of file + certkeyfile: /opt/nuts/certificate-and-key.pem \ No newline at end of file diff --git a/e2e-tests/oauth-flow/rfc002/node-A/nuts.yaml b/e2e-tests/oauth-flow/rfc002/node-A/nuts.yaml index 4ba88057d..18055e872 100644 --- a/e2e-tests/oauth-flow/rfc002/node-A/nuts.yaml +++ b/e2e-tests/oauth-flow/rfc002/node-A/nuts.yaml @@ -1,4 +1,5 @@ url: http://node-A +didmethods: ["nuts"] verbosity: debug strictmode: false internalratelimiter: false @@ -18,6 +19,3 @@ tls: network: v2: gossipinterval: 500 -vdr: - didmethods: - - nuts diff --git a/e2e-tests/oauth-flow/rfc002/node-B/nuts.yaml b/e2e-tests/oauth-flow/rfc002/node-B/nuts.yaml index 2aea7c3f3..d14c7e50a 100644 --- a/e2e-tests/oauth-flow/rfc002/node-B/nuts.yaml +++ b/e2e-tests/oauth-flow/rfc002/node-B/nuts.yaml @@ -1,4 +1,5 @@ url: http://node-b +didmethods: ["nuts"] verbosity: debug strictmode: false internalratelimiter: false @@ -18,7 +19,4 @@ tls: network: bootstrapnodes: nodeA-backend:5555 v2: - gossipinterval: 400 -vdr: - didmethods: - - nuts \ No newline at end of file + gossipinterval: 400 \ No newline at end of file diff --git a/e2e-tests/oauth-flow/statuslist2021/node-A/nuts.yaml b/e2e-tests/oauth-flow/statuslist2021/node-A/nuts.yaml index 8ec57a247..76f678287 100644 --- a/e2e-tests/oauth-flow/statuslist2021/node-A/nuts.yaml +++ b/e2e-tests/oauth-flow/statuslist2021/node-A/nuts.yaml @@ -1,4 +1,5 @@ url: https://nodeA +didmethods: ["web"] verbosity: debug strictmode: false internalratelimiter: false @@ -11,6 +12,3 @@ auth: - dummy irma: autoupdateschemas: false -vdr: - didmethods: - - web diff --git a/e2e-tests/oauth-flow/statuslist2021/node-B/nuts.yaml b/e2e-tests/oauth-flow/statuslist2021/node-B/nuts.yaml index 698a34450..b8e84cece 100644 --- a/e2e-tests/oauth-flow/statuslist2021/node-B/nuts.yaml +++ b/e2e-tests/oauth-flow/statuslist2021/node-B/nuts.yaml @@ -1,4 +1,5 @@ url: https://nodeB +didmethods: ["web"] verbosity: debug strictmode: false internalratelimiter: false @@ -10,7 +11,4 @@ auth: contractvalidators: - dummy irma: - autoupdateschemas: false -vdr: - didmethods: - - web \ No newline at end of file + autoupdateschemas: false \ No newline at end of file diff --git a/main_test.go b/main_test.go index 0a005594c..6c86314d8 100644 --- a/main_test.go +++ b/main_test.go @@ -34,7 +34,6 @@ import ( "github.com/nuts-foundation/nuts-node/network" "github.com/nuts-foundation/nuts-node/test" "github.com/nuts-foundation/nuts-node/test/pki" - "github.com/nuts-foundation/nuts-node/vdr" v1 "github.com/nuts-foundation/nuts-node/vdr/api/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -211,6 +210,7 @@ func getIntegrationTestConfig(t *testing.T, testDirectory string) (core.ServerCo config.TLS.CertFile = pki.CertificateFile(t) config.TLS.CertKeyFile = config.TLS.CertFile config.TLS.TrustStoreFile = pki.TruststoreFile(t) + config.DIDMethods = []string{"nuts"} config.Datadir = testDirectory @@ -230,16 +230,12 @@ func getIntegrationTestConfig(t *testing.T, testDirectory string) (core.ServerCo httpConfig.Internal.Address = fmt.Sprintf("localhost:%d", test.FreeTCPPort()) httpConfig.Public.Address = fmt.Sprintf("localhost:%d", test.FreeTCPPort()) - vdrConfig := vdr.DefaultConfig() - vdrConfig.DIDMethods = []string{"nuts"} - return config, ModuleConfig{ Network: networkConfig, Auth: authConfig, Crypto: cryptoConfig, Events: eventsConfig, HTTP: httpConfig, - VDR: vdrConfig, } } @@ -249,5 +245,4 @@ type ModuleConfig struct { Crypto crypto.Config `koanf:"crypto"` Events events.Config `koanf:"events"` HTTP httpEngine.Config `koanf:"http"` - VDR vdr.Config `koanf:"vdr"` } diff --git a/test/node/server.go b/test/node/server.go index 5043f8951..7433f13b8 100644 --- a/test/node/server.go +++ b/test/node/server.go @@ -75,7 +75,7 @@ func StartServer(t *testing.T, configFunc ...func(internalHttpServerURL, publicH t.Setenv("NUTS_EVENTS_NATS_PORT", natsPort) t.Setenv("NUTS_EVENTS_NATS_HOSTNAME", "localhost") t.Setenv("NUTS_URL", publicHttpServerURL) - t.Setenv("NUTS_VDR_DIDMETHODS", "nuts") + t.Setenv("NUTS_DIDMETHODS", "nuts") for _, fn := range configFunc { fn(internalHttpServerURL, publicHttpServerURL) diff --git a/vcr/test.go b/vcr/test.go index e8ed4811e..52062bee7 100644 --- a/vcr/test.go +++ b/vcr/test.go @@ -64,7 +64,6 @@ func NewTestVCRContext(t *testing.T, keyStore crypto.KeyStore) TestVCRContext { networkInstance := network.NewTestNetworkInstance(t) eventManager := events.NewTestManager(t) vdrInstance := vdr.NewVDR(keyStore, networkInstance, didStore, eventManager, storageEngine) - vdrInstance.Config().(*vdr.Config).DIDMethods = []string{"web", "nuts"} err := vdrInstance.Configure(core.TestServerConfig()) require.NoError(t, err) newInstance := NewVCRInstance( @@ -105,7 +104,6 @@ func NewTestVCRInstance(t *testing.T) *vcr { }) _ = networkInstance.Configure(serverCfg) vdrInstance := vdr.NewVDR(keyStore, networkInstance, didStore, eventManager, storageEngine) - vdrInstance.Config().(*vdr.Config).DIDMethods = []string{"web", "nuts"} err := vdrInstance.Configure(serverCfg) if err != nil { t.Fatal(err) @@ -135,7 +133,6 @@ func NewTestVCRInstanceInDir(t *testing.T, testDirectory string) *vcr { networkInstance := network.NewTestNetworkInstance(t) eventManager := events.NewTestManager(t) vdrInstance := vdr.NewVDR(nil, networkInstance, didStore, eventManager, storageEngine) - vdrInstance.Config().(*vdr.Config).DIDMethods = []string{"web", "nuts"} err := vdrInstance.Configure(core.TestServerConfig()) if err != nil { t.Fatal(err) diff --git a/vdr/cmd/cmd.go b/vdr/cmd/cmd.go index da0c7d857..5fad55990 100644 --- a/vdr/cmd/cmd.go +++ b/vdr/cmd/cmd.go @@ -30,26 +30,13 @@ import ( "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/core" - "github.com/nuts-foundation/nuts-node/vdr" api "github.com/nuts-foundation/nuts-node/vdr/api/v1" apiv2 "github.com/nuts-foundation/nuts-node/vdr/api/v2" "github.com/nuts-foundation/nuts-node/vdr/didnuts" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) -// FlagSet contains flags relevant for the VDR instance -func FlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("vdr", pflag.ContinueOnError) - - defs := vdr.DefaultConfig() - - flagSet.StringSlice("vdr.didmethods", defs.DIDMethods, "Comma-separated list of enabled DID methods (without did: prefix). "+ - "It also controls the order in which DIDs are returned by APIs, and which DID is used for signing if the verifying party does not impose restrictions on the DID method used.") - return flagSet -} - // Cmd contains sub-commands for the remote client func Cmd() *cobra.Command { cmd := &cobra.Command{ diff --git a/vdr/cmd/cmd_test.go b/vdr/cmd/cmd_test.go index 2424f615a..ab8f7ef82 100644 --- a/vdr/cmd/cmd_test.go +++ b/vdr/cmd/cmd_test.go @@ -40,10 +40,6 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_flagSet(t *testing.T) { - assert.NotNil(t, FlagSet()) -} - func TestEngine_Command(t *testing.T) { exampleID, _ := did.ParseDID("did:nuts:Fx8kamg7Bom4gyEzmJc9t9QmWTkCwSxu3mrp3CbkehR7") exampleDIDDocument := did.Document{ diff --git a/vdr/config.go b/vdr/config.go deleted file mode 100644 index 4f1123f33..000000000 --- a/vdr/config.go +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2024 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 . - * - */ - -package vdr - -type Config struct { - DIDMethods []string `koanf:"didmethods"` -} - -func DefaultConfig() Config { - return Config{ - DIDMethods: []string{"web", "nuts"}, - } -} diff --git a/vdr/didsubject/mock.go b/vdr/didsubject/mock.go index 564ea3395..77e2250a2 100644 --- a/vdr/didsubject/mock.go +++ b/vdr/didsubject/mock.go @@ -359,6 +359,20 @@ func (m *MockDocumentMigration) EXPECT() *MockDocumentMigrationMockRecorder { return m.recorder } +// MigrateAddWebToNuts mocks base method. +func (m *MockDocumentMigration) MigrateAddWebToNuts(ctx context.Context, id did.DID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MigrateAddWebToNuts", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// MigrateAddWebToNuts indicates an expected call of MigrateAddWebToNuts. +func (mr *MockDocumentMigrationMockRecorder) MigrateAddWebToNuts(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MigrateAddWebToNuts", reflect.TypeOf((*MockDocumentMigration)(nil).MigrateAddWebToNuts), ctx, id) +} + // MigrateDIDHistoryToSQL mocks base method. func (m *MockDocumentMigration) MigrateDIDHistoryToSQL(id did.DID, subject string, getHistory func(did.DID, int) ([]orm.MigrationDocument, error)) error { m.ctrl.T.Helper() diff --git a/vdr/interface.go b/vdr/interface.go index 78d275e26..0c1996730 100644 --- a/vdr/interface.go +++ b/vdr/interface.go @@ -37,8 +37,6 @@ type VDR interface { ResolveManaged(id did.DID) (*did.Document, error) // Resolver returns the resolver for getting the DID document for a DID. Resolver() resolver.DIDResolver - // SupportedMethods returns the activated DID methods. - SupportedMethods() []string // ConflictedDocuments returns the DID Document and metadata of all documents with a conflict. ConflictedDocuments() ([]did.Document, []resolver.DocumentMetadata, error) // PublicURL returns the public URL of the Nuts node, which is used as base URL for web-based DIDs. diff --git a/vdr/legacy_integration_test.go b/vdr/legacy_integration_test.go index eec8df6ea..1e9f56d76 100644 --- a/vdr/legacy_integration_test.go +++ b/vdr/legacy_integration_test.go @@ -255,6 +255,7 @@ func setup(t *testing.T) testContext { config.Strictmode = false config.Verbosity = "trace" config.Datadir = testDir + config.DIDMethods = []string{"nuts"} }) // Configure the logger: @@ -297,7 +298,6 @@ func setup(t *testing.T) testContext { pkiValidator, ) vdr := NewVDR(cryptoInstance, nutsNetwork, didStore, eventPublisher, storageEngine) - vdr.Config().(*Config).DIDMethods = []string{"nuts"} // Configure require.NoError(t, vdr.Configure(nutsConfig)) diff --git a/vdr/mock.go b/vdr/mock.go index ad01a6c9f..d0e945d6e 100644 --- a/vdr/mock.go +++ b/vdr/mock.go @@ -128,17 +128,3 @@ func (mr *MockVDRMockRecorder) Resolver() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolver", reflect.TypeOf((*MockVDR)(nil).Resolver)) } - -// SupportedMethods mocks base method. -func (m *MockVDR) SupportedMethods() []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SupportedMethods") - ret0, _ := ret[0].([]string) - return ret0 -} - -// SupportedMethods indicates an expected call of SupportedMethods. -func (mr *MockVDRMockRecorder) SupportedMethods() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedMethods", reflect.TypeOf((*MockVDR)(nil).SupportedMethods)) -} diff --git a/vdr/vdr.go b/vdr/vdr.go index 13f23fcca..570b8c474 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -61,12 +61,12 @@ var _ didsubject.Manager = (*Module)(nil) // It connects the Resolve, Create and Update DID methods to the network, and receives events back from the network which are processed in the store. // It is also a Runnable, Diagnosable and Configurable Nuts Engine. type Module struct { - config Config - publicURL *url.URL - store didnutsStore.Store - network network.Transactions - networkAmbassador didnuts.Ambassador - documentOwner didsubject.DocumentOwner + supportedDIDMethods []string + publicURL *url.URL + store didnutsStore.Store + network network.Transactions + networkAmbassador didnuts.Ambassador + documentOwner didsubject.DocumentOwner // nutsDocumentManager is used to manage did:nuts DID Documents // Deprecated: used by v1 api nutsDocumentManager didsubject.DocumentManager @@ -109,10 +109,6 @@ func (r *Module) Resolver() resolver.DIDResolver { return r.didResolver } -func (r *Module) SupportedMethods() []string { - return r.config.DIDMethods -} - // NewVDR creates a new Module with provided params func NewVDR(cryptoClient crypto.KeyStore, networkClient network.Transactions, didStore didnutsStore.Store, eventManager events.Event, storageInstance storage.Engine) *Module { @@ -134,22 +130,19 @@ func (r *Module) Name() string { return ModuleName } -func (r *Module) Config() interface{} { - return &r.config -} - // Configure configures the Module engine. func (r *Module) Configure(config core.ServerConfig) error { + r.supportedDIDMethods = config.DIDMethods var err error if r.publicURL, err = config.ServerURL(); err != nil { return err } // at least one method should be configured - if len(r.config.DIDMethods) == 0 { + if len(r.supportedDIDMethods) == 0 { return errors.New("at least one DID method should be configured") } // check if all configured methods are supported - for _, method := range r.config.DIDMethods { + for _, method := range r.supportedDIDMethods { switch method { case didnuts.MethodName, didweb.MethodName: continue @@ -179,7 +172,7 @@ func (r *Module) Configure(config core.ServerConfig) error { // did:nuts nutsManager := didnuts.NewManager(r.keyStore, r.network, r.store, r.didResolver, db) r.nutsDocumentManager = nutsManager - if slices.Contains(r.config.DIDMethods, didnuts.MethodName) { + if slices.Contains(r.supportedDIDMethods, didnuts.MethodName) { methodManagers[didnuts.MethodName] = nutsManager r.didResolver.(*resolver.DIDResolverRouter).Register(didnuts.MethodName, &didnuts.Resolver{Store: r.store}) } @@ -201,12 +194,12 @@ func (r *Module) Configure(config core.ServerConfig) error { didweb.NewResolver(), }, } - if slices.Contains(r.config.DIDMethods, didweb.MethodName) { + if slices.Contains(r.supportedDIDMethods, didweb.MethodName) { methodManagers[didweb.MethodName] = webManager r.didResolver.(*resolver.DIDResolverRouter).Register(didweb.MethodName, webResolver) } - r.Manager = didsubject.New(db, methodManagers, r.keyStore, r.config.DIDMethods) + r.Manager = didsubject.New(db, methodManagers, r.keyStore, r.supportedDIDMethods) // Initiate the routines for auto-updating the data. return r.networkAmbassador.Configure() @@ -367,7 +360,7 @@ func (r *Module) Migrate() error { } // only migrate if did:nuts is activated on the node - if slices.Contains(r.SupportedMethods(), "nuts") { + if slices.Contains(r.supportedDIDMethods, "nuts") { for _, m := range r.migrations { log.Logger().Infof("Running did:nuts migration: '%s'", m.name) m.migrate(owned) @@ -452,7 +445,7 @@ func (r *Module) migrateHistoryOwnedDIDNuts(owned []did.DID) { } func (r *Module) migrateAddDIDWebToOwnedDIDNuts(owned []did.DID) { - if !slices.Contains(r.SupportedMethods(), "web") { + if !slices.Contains(r.supportedDIDMethods, "web") { log.Logger().Info("did:web not in supported did methods. Abort migration.") return } diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index 7b31545b6..1c57bdc8f 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -86,7 +86,7 @@ func newVDRTestCtx(t *testing.T) vdrTestCtx { DB: db, MethodManagers: make(map[string]didsubject.MethodManager), } - vdr.config = DefaultConfig() + vdr.supportedDIDMethods = []string{"web", "nuts"} resolverRouter.Register(didnuts.MethodName, &didnuts.Resolver{Store: mockStore}) return vdrTestCtx{ ctrl: ctrl, @@ -176,7 +176,6 @@ func TestVDR_ConflictingDocuments(t *testing.T) { keyID.Fragment = "1" _, _, _ = client.New(audit.TestContext(), nutsCrypto.StringNamingFunc(keyID.String())) vdr := NewVDR(client, nil, didstore.NewTestStore(t), nil, storageEngine) - vdr.Config().(*Config).DIDMethods = []string{"web", "nuts"} _ = vdr.Configure(core.TestServerConfig()) didDocument := did.Document{ID: TestDIDA} @@ -211,7 +210,6 @@ func TestVDR_ConflictingDocuments(t *testing.T) { // change vdr to allow for Configure() vdr := NewVDR(test.keyStore, nil, didstore.NewTestStore(t), nil, test.storageEngine) tmpResolver := vdr.didResolver - vdr.Config().(*Config).DIDMethods = []string{"web", "nuts"} _ = vdr.Configure(core.TestServerConfig()) vdr.didResolver = tmpResolver @@ -264,8 +262,7 @@ func TestVDR_Configure(t *testing.T) { }) instance := NewVDR(nil, nil, nil, nil, storageInstance) - instance.Config().(*Config).DIDMethods = []string{"web", "nuts"} - err := instance.Configure(core.ServerConfig{URL: "https://nuts.nl"}) + err := instance.Configure(core.ServerConfig{URL: "https://nuts.nl", DIDMethods: []string{"web", "nuts"}}) require.NoError(t, err) doc, md, err := instance.Resolver().Resolve(did.MustParseDID("did:web:example.com"), nil) @@ -277,8 +274,7 @@ func TestVDR_Configure(t *testing.T) { t.Run("resolves local DID from database", func(t *testing.T) { db := storageInstance.GetSQLDatabase() instance := NewVDR(nutsCrypto.NewDatabaseCryptoInstance(db), nil, nil, nil, storageInstance) - instance.Config().(*Config).DIDMethods = []string{"web", "nuts"} - err := instance.Configure(core.ServerConfig{URL: "https://example.com"}) + err := instance.Configure(core.ServerConfig{URL: "https://example.com", DIDMethods: []string{"web", "nuts"}}) require.NoError(t, err) sqlDIDDocumentManager := didsubject.NewDIDDocumentManager(db) sqlDID := orm.DID{ @@ -306,7 +302,6 @@ func TestVDR_Configure(t *testing.T) { require.NoError(t, err) instance := NewVDR(nil, nil, nil, nil, storageInstance) - instance.Config().(*Config).DIDMethods = []string{"web", "nuts"} err = instance.Configure(core.TestServerConfig()) require.NoError(t, err) @@ -321,7 +316,6 @@ func TestVDR_Configure(t *testing.T) { }) t.Run("it can resolve using did:key", func(t *testing.T) { instance := NewVDR(nil, nil, nil, nil, storageInstance) - instance.Config().(*Config).DIDMethods = []string{"web", "nuts"} err := instance.Configure(core.TestServerConfig()) require.NoError(t, err) @@ -486,7 +480,7 @@ func TestVDR_Migrate(t *testing.T) { defer func() { logrus.StandardLogger().Level = logrus.WarnLevel }() ctx := didwebMigrationSetup(t) ctx.vdr.migrations = []migration{{ctx.vdr.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}} - ctx.vdr.config.DIDMethods = []string{"nuts"} + ctx.vdr.supportedDIDMethods = []string{"nuts"} ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{TestDIDA}, nil) err := ctx.vdr.Migrate() From c39b68c8542d781ce3cf961676a87eaa38e1862b Mon Sep 17 00:00:00 2001 From: reinkrul Date: Fri, 4 Oct 2024 10:27:38 +0200 Subject: [PATCH 15/72] Discovery: fix GetActivationStatus on MS SQL Server (#3443) --- discovery/store.go | 11 +++++++++-- e2e-tests/oauth-flow/rfc021/do-test.sh | 13 +++++++++++++ .../rfc021/shared/discovery/definition.json | 6 ++++-- vcr/revocation/statuslist2021_issuer.go | 2 ++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/discovery/store.go b/discovery/store.go index 87bb94089..9e0b3780e 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -89,7 +89,9 @@ type presentationRefreshRecord struct { // Parameters is a serialized JSON object containing parameters that should be used when registering the subject on the service. Parameters []byte // PresentationRefreshError is the error message that occurred during the refresh attempt. - PresentationRefreshError presentationRefreshError `gorm:"foreignKey:ServiceID,SubjectID"` + // It's loaded using a spearate query instead of using GORM's Preload, which fails on MS SQL Server if it spans multiple columns + // See https://github.com/nuts-foundation/nuts-node/issues/3442 + PresentationRefreshError presentationRefreshError `gorm:"-"` } // TableName returns the table name for this DTO. @@ -353,12 +355,17 @@ func (s *sqlStore) updatePresentationRefreshTime(serviceID string, subjectID str func (s *sqlStore) getPresentationRefreshRecord(serviceID string, subjectID string) (*presentationRefreshRecord, error) { var row presentationRefreshRecord - if err := s.db.Preload("PresentationRefreshError").Find(&row, "service_id = ? AND subject_id = ?", serviceID, subjectID).Error; err != nil { + if err := s.db.Find(&row, "service_id = ? AND subject_id = ?", serviceID, subjectID).Error; err != nil { return nil, err } if row.NextRefresh == 0 { return nil, nil } + // Load presentationRefreshError using a spearate query instead of using GORM's Preload, which fails on MS SQL Server if it spans multiple columns + // See https://github.com/nuts-foundation/nuts-node/issues/3442 + if err := s.db.Find(&row.PresentationRefreshError, "service_id = ? AND subject_id = ?", serviceID, subjectID).Error; err != nil { + return nil, err + } return &row, nil } diff --git a/e2e-tests/oauth-flow/rfc021/do-test.sh b/e2e-tests/oauth-flow/rfc021/do-test.sh index 86c7273d7..c28fce6bf 100755 --- a/e2e-tests/oauth-flow/rfc021/do-test.sh +++ b/e2e-tests/oauth-flow/rfc021/do-test.sh @@ -75,6 +75,19 @@ else exitWithDockerLogs 1 fi +# Test regression for https://github.com/nuts-foundation/nuts-node/issues/3442 +# (Discovery: GetActivationStatus fails on MS SQL Server) +echo "Getting activation status from Discovery Service..." +RESPONSE=$(curl -s http://localhost:28081/internal/discovery/v1/e2e-test/vendorB) +echo $RESPONSE +if echo $RESPONSE | grep -q "true"; then + echo "Activation status OK" +else + echo "FAILED: Could not get activation status of vendor B on Discovery Service" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + echo "---------------------------------------" echo "Perform OAuth 2.0 rfc021 flow..." echo "---------------------------------------" diff --git a/e2e-tests/oauth-flow/rfc021/shared/discovery/definition.json b/e2e-tests/oauth-flow/rfc021/shared/discovery/definition.json index e7b38ad1c..f43a88422 100644 --- a/e2e-tests/oauth-flow/rfc021/shared/discovery/definition.json +++ b/e2e-tests/oauth-flow/rfc021/shared/discovery/definition.json @@ -27,7 +27,8 @@ }, { "path": [ - "$.credentialSubject.organization.name" + "$.credentialSubject.organization.name", + "$.credentialSubject[0].organization.name" ], "filter": { "type": "string" @@ -35,7 +36,8 @@ }, { "path": [ - "$.credentialSubject.organization.city" + "$.credentialSubject.organization.city", + "$.credentialSubject[0].organization.city" ], "filter": { "type": "string" diff --git a/vcr/revocation/statuslist2021_issuer.go b/vcr/revocation/statuslist2021_issuer.go index d8946eb1b..9ea538a11 100644 --- a/vcr/revocation/statuslist2021_issuer.go +++ b/vcr/revocation/statuslist2021_issuer.go @@ -121,6 +121,7 @@ func (cs *StatusList2021) isManaged(subjectID string) bool { var exists bool cs.db.Model(new(credentialIssuerRecord)). Select("count(*) > 0"). + Group("subject_id"). Where("subject_id = ?", subjectID). First(&exists) return exists @@ -423,6 +424,7 @@ func (cs *StatusList2021) Revoke(ctx context.Context, credentialID ssi.URI, entr err = tx.Model(new(credentialRecord)). Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate}). Select("count(*) > 0"). + Group("subject_id"). Where("subject_id = ?", entry.StatusListCredential). First(new(bool)). Error From 1e3e2c1f336ae5464dbe51b095850ab2b8e3b3b2 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:37:43 +0200 Subject: [PATCH 16/72] disable did:web migration (#3448) --- e2e-tests/migration/main_test.go | 3 ++- vdr/vdr.go | 3 ++- vdr/vdr_test.go | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/e2e-tests/migration/main_test.go b/e2e-tests/migration/main_test.go index e877e8be9..7532897ec 100644 --- a/e2e-tests/migration/main_test.go +++ b/e2e-tests/migration/main_test.go @@ -47,7 +47,7 @@ func Test_Migrations(t *testing.T) { DIDs, err := man.DID.All() require.NoError(t, err) - require.Len(t, DIDs, 7) // 4 did:nuts, 3 did:web + require.Len(t, DIDs, 4) // 4 did:nuts, 3 did:web t.Run("vendor", func(t *testing.T) { // versions for did:nuts: @@ -138,6 +138,7 @@ func Test_Migrations(t *testing.T) { } func EqualServices(t *testing.T, man *manager, nutsDoc *orm.DidDocument) { + return // disable until there is a fix for https://github.com/nuts-foundation/nuts-node/issues/3444 didWebPrefix := "did:web:nodeA%3A8080" dids, err := man.DID.FindBySubject(nutsDoc.DID.Subject) // migrated documents have subject == did:nuts:... diff --git a/vdr/vdr.go b/vdr/vdr.go index 570b8c474..22bea69d8 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -382,7 +382,8 @@ func (r *Module) allMigrations() []migration { return []migration{ // key will be printed as description of the migration {r.migrateRemoveControllerFromDIDNuts, "remove controller"}, // must come before migrateHistoryOwnedDIDNuts so controller removal is also migrated. {r.migrateHistoryOwnedDIDNuts, "document history"}, - {r.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}, // must come after migrateHistoryOwnedDIDNuts since it acts on the SQL store. + // Disable migration until we have a fix for: https://github.com/nuts-foundation/nuts-node/issues/3444 + //{r.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}, // must come after migrateHistoryOwnedDIDNuts since it acts on the SQL store. } } diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index 1c57bdc8f..2eebeb9b5 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -348,7 +348,7 @@ func TestVDR_Migrate(t *testing.T) { ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{testDIDWeb}, nil) err := ctx.vdr.Migrate() assert.NoError(t, err) - assert.Len(t, ctx.vdr.migrations, 3) // confirm its running allMigrations() that currently is only did:nuts + assert.Len(t, ctx.vdr.migrations, 2) // confirm its running allMigrations() that currently is only did:nuts }) t.Run("controller migration", func(t *testing.T) { controllerMigrationSetup := func(t *testing.T) vdrTestCtx { From 9ec13e7b0f57229e21758584bc9df679d96351cd Mon Sep 17 00:00:00 2001 From: reinkrul Date: Fri, 4 Oct 2024 10:40:18 +0200 Subject: [PATCH 17/72] Discovery: do not log 'missing credentials' if other DIDs are successfully registered (#3447) --- discovery/client.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/discovery/client.go b/discovery/client.go index cfefdf88d..6b4a7b4d1 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -99,7 +99,12 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service for _, subjectDID := range subjectDIDs { err := r.registerPresentation(ctx, subjectDID, service, parameters) if err != nil { - loopErrs = append(loopErrs, fmt.Errorf("%s: %w", subjectDID.String(), err)) + if !errors.Is(err, pe.ErrNoCredentials) { // ignore missing credentials + loopErrs = append(loopErrs, fmt.Errorf("%s: %w", subjectDID.String(), err)) + } else { + // trace logging for missing credentials + log.Logger().Tracef("Missing credentials for Discovery Service (service=%s, subject=%s, did=%s): %s", service.ID, subjectID, subjectDID, err.Error()) + } } else { registeredDIDs = append(registeredDIDs, subjectDID.String()) } From 4ad18cce6cc3e3e8c9901aefd82183231f779e94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:42:32 +0200 Subject: [PATCH 18/72] Bump github.com/knadh/koanf/providers/file from 1.1.0 to 1.1.1 (#3445) Bumps [github.com/knadh/koanf/providers/file](https://github.com/knadh/koanf) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/knadh/koanf/releases) - [Commits](https://github.com/knadh/koanf/compare/v1.1.0...v1.1.1) --- updated-dependencies: - dependency-name: github.com/knadh/koanf/providers/file dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 9 ++++----- go.sum | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 4249b9084..2cfb089bb 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/vault/api v1.15.0 github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/env v1.0.0 - github.com/knadh/koanf/providers/file v1.1.0 + github.com/knadh/koanf/providers/file v1.1.1 github.com/knadh/koanf/providers/posflag v0.1.0 github.com/knadh/koanf/providers/structs v0.1.0 github.com/labstack/echo/v4 v4.12.0 @@ -204,7 +204,6 @@ require ( rsc.io/qr v0.2.0 // indirect ) -require ( - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect - github.com/knadh/koanf/v2 v2.1.1 // indirect -) +require github.com/knadh/koanf/v2 v2.1.1 + +require github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect diff --git a/go.sum b/go.sum index 4da4589e6..18dfb848e 100644 --- a/go.sum +++ b/go.sum @@ -272,8 +272,8 @@ github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= -github.com/knadh/koanf/providers/file v1.1.0 h1:MTjA+gRrVl1zqgetEAIaXHqYje0XSosxSiMD4/7kz0o= -github.com/knadh/koanf/providers/file v1.1.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/providers/file v1.1.1 h1:AyYV0SMrEzfw5O3cX5pGmKfq61/bhj19u7OQeFH5AHs= +github.com/knadh/koanf/providers/file v1.1.1/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= From 07285807812aec5b0273a1590e7a6622d5468a54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:35:11 +0200 Subject: [PATCH 19/72] Bump github.com/knadh/koanf/providers/file from 1.1.1 to 1.1.2 (#3450) Bumps [github.com/knadh/koanf/providers/file](https://github.com/knadh/koanf) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/knadh/koanf/releases) - [Commits](https://github.com/knadh/koanf/compare/v1.1.1...providers/file/v1.1.2) --- updated-dependencies: - dependency-name: github.com/knadh/koanf/providers/file dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2cfb089bb..804181a00 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/vault/api v1.15.0 github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/env v1.0.0 - github.com/knadh/koanf/providers/file v1.1.1 + github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/providers/posflag v0.1.0 github.com/knadh/koanf/providers/structs v0.1.0 github.com/labstack/echo/v4 v4.12.0 diff --git a/go.sum b/go.sum index 18dfb848e..9ecfd518f 100644 --- a/go.sum +++ b/go.sum @@ -272,8 +272,8 @@ github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= -github.com/knadh/koanf/providers/file v1.1.1 h1:AyYV0SMrEzfw5O3cX5pGmKfq61/bhj19u7OQeFH5AHs= -github.com/knadh/koanf/providers/file v1.1.1/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= +github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= From b7e55303d53178a5fcb906d8d85ef3a129c106b0 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Mon, 7 Oct 2024 14:36:30 +0200 Subject: [PATCH 20/72] VCR: Fix broken First() operation which breaks retrieving StatusList credential on MS SQL Server (#3452) Signed-off-by: Rein Krul --- e2e-tests/oauth-flow/rfc021/do-test.sh | 26 +++++++++++++++++++------ vcr/revocation/statuslist2021_issuer.go | 10 +++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/e2e-tests/oauth-flow/rfc021/do-test.sh b/e2e-tests/oauth-flow/rfc021/do-test.sh index c28fce6bf..a91234153 100755 --- a/e2e-tests/oauth-flow/rfc021/do-test.sh +++ b/e2e-tests/oauth-flow/rfc021/do-test.sh @@ -45,16 +45,16 @@ echo Vendor B DID: $VENDOR_B_DID # Issue NutsOrganizationCredential for Vendor B REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${VENDOR_B_DID}\", \"credentialSubject\": {\"id\":\"${VENDOR_B_DID}\", \"organization\":{\"name\":\"Caresoft B.V.\", \"city\":\"Caretown\"}},\"withStatusList2021Revocation\": true}" -RESPONSE=$(echo $REQUEST | curl -X POST --data-binary @- http://localhost:28081/internal/vcr/v2/issuer/vc -H "Content-Type:application/json") -if echo $RESPONSE | grep -q "VerifiableCredential"; then +VENDOR_B_CREDENTIAL=$(echo $REQUEST | curl -X POST --data-binary @- http://localhost:28081/internal/vcr/v2/issuer/vc -H "Content-Type:application/json") +if echo $VENDOR_B_CREDENTIAL | grep -q "VerifiableCredential"; then echo "VC issued" else echo "FAILED: Could not issue NutsOrganizationCredential to node-B" 1>&2 - echo $RESPONSE + echo $VENDOR_B_CREDENTIAL exitWithDockerLogs 1 fi -RESPONSE=$(echo $RESPONSE | curl -X POST --data-binary @- http://localhost:28081/internal/vcr/v2/holder/vendorB/vc -H "Content-Type:application/json") +RESPONSE=$(echo $VENDOR_B_CREDENTIAL | curl -X POST --data-binary @- http://localhost:28081/internal/vcr/v2/holder/vendorB/vc -H "Content-Type:application/json") if echo $RESPONSE == ""; then echo "VC stored in wallet" else @@ -63,6 +63,22 @@ else exitWithDockerLogs 1 fi +# Test regression for https://github.com/nuts-foundation/nuts-node/issues/3451 +# (VCR: Status List can't be retrieved when using MS SQL Server) +# Get credential status URL from credentialStatus.statusListCredential property using jq +STATUS_LIST_CREDENTIAL=$(echo $VENDOR_B_CREDENTIAL | jq -r .credentialStatus.statusListCredential) +echo "Status list credential: $STATUS_LIST_CREDENTIAL" +# Get status list credential +RESPONSE=$($db_dc exec nodeB-backend curl -s -k $STATUS_LIST_CREDENTIAL) +# Check response HTTP 200 OK +if [ $? -eq 0 ]; then + echo "Status list credential retrieved" +else + echo "FAILED: Could not retrieve status list credential" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + # Register vendor B on Discovery Service echo "Registering vendor B on Discovery Service..." REQUEST="{\"registrationParameters\":{\"key\":\"value\"}}" @@ -167,8 +183,6 @@ else exitWithDockerLogs 1 fi - - echo "------------------------------------" echo "Retrieving data..." echo "------------------------------------" diff --git a/vcr/revocation/statuslist2021_issuer.go b/vcr/revocation/statuslist2021_issuer.go index 9ea538a11..cd02ca911 100644 --- a/vcr/revocation/statuslist2021_issuer.go +++ b/vcr/revocation/statuslist2021_issuer.go @@ -118,13 +118,13 @@ func (cs *StatusList2021) loadCredential(subjectID string) (*credentialRecord, e // isManaged returns true if the StatusList2021Credential is issued by this node. // returns false on db errors, or if the StatusList2021Credential does not exist. func (cs *StatusList2021) isManaged(subjectID string) bool { - var exists bool + var count int cs.db.Model(new(credentialIssuerRecord)). - Select("count(*) > 0"). + Select("count(*)"). Group("subject_id"). Where("subject_id = ?", subjectID). - First(&exists) - return exists + Find(&count) + return count > 0 } func (cs *StatusList2021) Credential(ctx context.Context, issuerDID did.DID, page int) (*vc.VerifiableCredential, error) { @@ -426,7 +426,7 @@ func (cs *StatusList2021) Revoke(ctx context.Context, credentialID ssi.URI, entr Select("count(*) > 0"). Group("subject_id"). Where("subject_id = ?", entry.StatusListCredential). - First(new(bool)). + Find(new([]bool)). Error if err != nil { return err From ecd74bbaab2015ad55bdc376b03ac62adf7ee25b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:55:07 +0200 Subject: [PATCH 21/72] Bump golang.org/x/time from 0.6.0 to 0.7.0 (#3453) Bumps [golang.org/x/time](https://github.com/golang/time) from 0.6.0 to 0.7.0. - [Commits](https://github.com/golang/time/compare/v0.6.0...v0.7.0) --- updated-dependencies: - dependency-name: golang.org/x/time dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 804181a00..5113b3063 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/mock v0.4.0 golang.org/x/crypto v0.27.0 - golang.org/x/time v0.6.0 + golang.org/x/time v0.7.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.34.2 gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 diff --git a/go.sum b/go.sum index 9ecfd518f..159ea2b68 100644 --- a/go.sum +++ b/go.sum @@ -616,8 +616,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= From 8111be257be43e8fcd875d400d931a5d602a03e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:05:36 +0200 Subject: [PATCH 22/72] Bump golang.org/x/crypto from 0.27.0 to 0.28.0 (#3454) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.28.0. - [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.28.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 5113b3063..62ce2c458 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.4.0 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.28.0 golang.org/x/time v0.7.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.34.2 @@ -191,9 +191,9 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 // indirect gorm.io/gorm v1.25.12 diff --git a/go.sum b/go.sum index 159ea2b68..1e3f26759 100644 --- a/go.sum +++ b/go.sum @@ -533,8 +533,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= @@ -596,16 +596,16 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -614,8 +614,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 48d37c1a2e6c6fff600d47cb09d23a2060e601c7 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Tue, 8 Oct 2024 10:32:31 +0200 Subject: [PATCH 23/72] VCR: fix MS SQL Server status list refresh (#3457) --- vcr/revocation/statuslist2021_issuer.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vcr/revocation/statuslist2021_issuer.go b/vcr/revocation/statuslist2021_issuer.go index cd02ca911..74d482c8c 100644 --- a/vcr/revocation/statuslist2021_issuer.go +++ b/vcr/revocation/statuslist2021_issuer.go @@ -170,7 +170,7 @@ func (cs *StatusList2021) Credential(ctx context.Context, issuerDID did.DID, pag // Microsoft SQL server does not support the locking clause, so we have to use a raw query instead. // See https://github.com/nuts-foundation/nuts-node/issues/3393 if tx.Dialector.Name() == "sqlserver" { - err = tx.Raw("SELECT * FROM status_list_entry WITH (UPDLOCK, ROWLOCK) WHERE subject_id = ?", statusListCredentialURL). + err = tx.Raw("SELECT * FROM status_list_credential WITH (UPDLOCK, ROWLOCK) WHERE subject_id = ?", statusListCredentialURL). Scan(new(credentialRecord)). Error } else { @@ -423,10 +423,9 @@ func (cs *StatusList2021) Revoke(ctx context.Context, credentialID ssi.URI, entr // lock relevant credentialRecord. It was created when the first entry was issued for this StatusList2021Credential. err = tx.Model(new(credentialRecord)). Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate}). - Select("count(*) > 0"). - Group("subject_id"). + Select("subject_id"). Where("subject_id = ?", entry.StatusListCredential). - Find(new([]bool)). + Find(new([]string)). Error if err != nil { return err From 7854e88887b815ca290c89af20ba054f6529f4e9 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 8 Oct 2024 10:35:28 +0200 Subject: [PATCH 24/72] prevent duplicate results in discovery search (#3459) --- discovery/store.go | 10 +++++++--- discovery/store_test.go | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/discovery/store.go b/discovery/store.go index 9e0b3780e..d09629185 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -229,10 +229,14 @@ func (s *sqlStore) get(serviceID string, startAfter int) (map[string]vc.Verifiab // Wildcard matching is supported by prefixing or suffixing the value with an asterisk (*). // It returns the presentations which contain credentials that match the given query. func (s *sqlStore) search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) { + // first only select columns also used in group by clause + // if the query is empty, there's no need to do a join stmt := s.db.Model(&presentationRecord{}). - Where("service_id = ?", serviceID). - Joins("inner join discovery_credential ON discovery_credential.presentation_id = discovery_presentation.id") - stmt = store.CredentialStore{}.BuildSearchStatement(stmt, "discovery_credential.credential_id", query) + Where("service_id = ?", serviceID) + if len(query) > 0 { + stmt = stmt.Joins("inner join discovery_credential ON discovery_credential.presentation_id = discovery_presentation.id") + stmt = store.CredentialStore{}.BuildSearchStatement(stmt, "discovery_credential.credential_id", query) + } var matches []presentationRecord if err := stmt.Preload("Credentials").Preload("Credentials.Credential").Find(&matches).Error; err != nil { diff --git a/discovery/store_test.go b/discovery/store_test.go index d675905a6..b782cb0a4 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -196,6 +196,18 @@ func Test_sqlStore_search(t *testing.T) { require.Len(t, actualVPs, 1) assert.Equal(t, vpAlice.ID.String(), actualVPs[0].ID.String()) }) + t.Run("find all", func(t *testing.T) { + vps := []vc.VerifiablePresentation{vpAlice, vpBob} + c := setupStore(t, storageEngine.GetSQLDatabase()) + for _, vp := range vps { + err := c.add(testServiceID, vp, 0) + require.NoError(t, err) + } + + actualVPs, err := c.search(testServiceID, map[string]string{}) + require.NoError(t, err) + require.Len(t, actualVPs, 2) + }) t.Run("not found", func(t *testing.T) { vps := []vc.VerifiablePresentation{vpAlice, vpBob} c := setupStore(t, storageEngine.GetSQLDatabase()) From a7386317dbfaec51168736fe07d584e17dbe6ecf Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:45:58 +0200 Subject: [PATCH 25/72] Disable network layer if 'didmethods' does not contain 'nuts' (#3449) * disable network layer if 'didmethods' does not contain 'nuts' --- network/api/v1/api.go | 17 ++++++++++++ network/interface.go | 5 ++-- network/mock.go | 14 ++++++++++ network/network.go | 60 ++++++++++++++++++++++++++++++++++++----- network/network_test.go | 7 +++-- vcr/test.go | 1 + vcr/vcr.go | 13 +++++++-- vcr/vcr_test.go | 4 ++- vdr/vdr.go | 14 ++++++++-- 9 files changed, 117 insertions(+), 18 deletions(-) diff --git a/network/api/v1/api.go b/network/api/v1/api.go index 5127066a5..8089f877c 100644 --- a/network/api/v1/api.go +++ b/network/api/v1/api.go @@ -25,6 +25,7 @@ import ( "github.com/labstack/echo/v4" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/network/log" + "net/http" "time" "github.com/nuts-foundation/nuts-node/core" @@ -42,10 +43,19 @@ type Wrapper struct { func (a *Wrapper) Routes(router core.EchoRouter) { RegisterHandlers(router, NewStrictHandler(a, []StrictMiddlewareFunc{ + func(f StrictHandlerFunc, operationID string) StrictHandlerFunc { // fast fail if did:nuts is disabled + return func(ctx echo.Context, args interface{}) (interface{}, error) { + if a.Service.Disabled() { + return nil, network.ErrDIDNutsDisabled + } + return f(ctx, args) + } + }, func(f StrictHandlerFunc, operationID string) StrictHandlerFunc { return func(ctx echo.Context, request interface{}) (response interface{}, err error) { ctx.Set(core.OperationIDContextKey, operationID) ctx.Set(core.ModuleNameContextKey, network.ModuleName) + ctx.Set(core.StatusCodeResolverContextKey, a) return f(ctx, request) } }, @@ -55,6 +65,13 @@ func (a *Wrapper) Routes(router core.EchoRouter) { })) } +// ResolveStatusCode maps errors returned by this API to specific HTTP status codes. +func (a *Wrapper) ResolveStatusCode(err error) int { + return core.ResolveStatusCode(err, map[error]int{ + network.ErrDIDNutsDisabled: http.StatusBadRequest, + }) +} + // ListTransactions lists all transactions func (a *Wrapper) ListTransactions(_ context.Context, request ListTransactionsRequestObject) (ListTransactionsResponseObject, error) { // Parse the start/end params, which have default values diff --git a/network/interface.go b/network/interface.go index ee49c2309..aef5ed4cf 100644 --- a/network/interface.go +++ b/network/interface.go @@ -61,14 +61,13 @@ type Transactions interface { DiscoverServices(updatedDID did.DID) // AddressBook returns the list of contacts in the address book. AddressBook() []transport.Contact + // Disabled returns true if core.ServerConfig.DIDMethods does not contain 'nuts' + Disabled() bool } // EventType defines a type for specifying the kind of events that can be published/subscribed on the Network. type EventType string -// AnyPayloadType is a wildcard that matches with any payload type. -const AnyPayloadType = "*" - // Receiver defines a callback function for processing transactions/payloads received by the DAG. type Receiver func(transaction dag.Transaction, payload []byte) error diff --git a/network/mock.go b/network/mock.go index f4ae30bfe..560aea9ae 100644 --- a/network/mock.go +++ b/network/mock.go @@ -86,6 +86,20 @@ func (mr *MockTransactionsMockRecorder) CreateTransaction(ctx, spec any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransaction", reflect.TypeOf((*MockTransactions)(nil).CreateTransaction), ctx, spec) } +// Disabled mocks base method. +func (m *MockTransactions) Disabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Disabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Disabled indicates an expected call of Disabled. +func (mr *MockTransactionsMockRecorder) Disabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disabled", reflect.TypeOf((*MockTransactions)(nil).Disabled)) +} + // DiscoverServices mocks base method. func (m *MockTransactions) DiscoverServices(updatedDID did.DID) { m.ctrl.T.Helper() diff --git a/network/network.go b/network/network.go index b573a7134..0f148d2fe 100644 --- a/network/network.go +++ b/network/network.go @@ -26,6 +26,7 @@ import ( "errors" "fmt" "net" + "slices" "strings" "sync/atomic" "time" @@ -66,11 +67,15 @@ const ( newNodeConnectionDelay = 5 * time.Minute ) +// ErrDIDNutsDisabled is returned from certain API methods when the core.ServerConfig.DIDMethods does not contain "nuts" +var ErrDIDNutsDisabled = errors.New("network operations not supported; did:nuts support not configured") + // defaultBBoltOptions are given to bbolt, allows for package local adjustments during test var defaultBBoltOptions = bbolt.DefaultOptions // Network implements Transactions interface and Engine functions. type Network struct { + disabled bool // node is running without did:nuts support config Config certificate tls.Certificate trustStore *core.TrustStore @@ -96,6 +101,9 @@ type Network struct { // CheckHealth performs health checks for the network engine. func (n *Network) CheckHealth() map[string]core.Health { + if n.disabled { + return nil + } results := make(map[string]core.Health) if n.certificate.Leaf != nil { results[healthTLS] = n.checkNodeTLSHealth() @@ -134,6 +142,9 @@ func (n *Network) checkNodeTLSHealth() core.Health { } func (n *Network) Migrate() error { + if n.disabled { + return nil + } return n.state.Migrate() } @@ -167,6 +178,10 @@ func NewNetworkInstance( // Configure configures the Network subsystem func (n *Network) Configure(config core.ServerConfig) error { + if !slices.Contains(config.DIDMethods, "nuts") { + n.disabled = true + return nil + } var err error dagStore, err := n.storeProvider.GetKVStore("data", storage.PersistentStorageClass) if err != nil { @@ -269,11 +284,7 @@ func (n *Network) Configure(config core.ServerConfig) error { } else { // Not allowed in strict mode for security reasons: only intended for demo/workshop purposes. if config.Strictmode { - if len(n.config.BootstrapNodes) == 0 && n.assumeNewNode { - log.Logger().Info("It appears the gRPC network will not be used (no bootstrap nodes and an empty network state), so disabled TLS is accepted even with strict mode enabled.") - } else { - return errors.New("disabling TLS in strict mode is not allowed") - } + return errors.New("disabling TLS in strict mode is not allowed") } authenticator = grpc.NewDummyAuthenticator(nil) } @@ -313,7 +324,7 @@ func (n *Network) Configure(config core.ServerConfig) error { } func (n *Network) DiscoverServices(updatedDID did.DID) { - if !n.config.EnableDiscovery { + if n.disabled || !n.config.EnableDiscovery { return } document, _, err := n.didStore.Resolve(updatedDID, nil) @@ -363,6 +374,9 @@ func (n *Network) Config() interface{} { // Start initiates the Network subsystem func (n *Network) Start() error { + if n.disabled { + return nil + } startTime := time.Now() n.startTime.Store(&startTime) @@ -571,6 +585,9 @@ func (n *Network) selfTestNutsCommAddress(nutsComm transport.NutsCommURL) error // The receiver is called when a transaction is added to the DAG. // It's only called if the given dag.NotificationFilter's match. func (n *Network) Subscribe(name string, subscriber dag.ReceiverFn, options ...SubscriberOption) error { + if n.disabled { + return nil + } notifierOptions := make([]dag.NotifierOption, len(options)) for i, o := range options { notifierOptions[i] = o() @@ -609,12 +626,18 @@ func (n *Network) CleanupSubscriberEvents(subscriberName, errorPrefix string) er // GetTransaction retrieves the transaction for the given reference. If the transaction is not known, an error is returned. func (n *Network) GetTransaction(transactionRef hash.SHA256Hash) (dag.Transaction, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } return n.state.GetTransaction(context.Background(), transactionRef) } // GetTransactionPayload retrieves the transaction Payload for the given transaction. If the transaction or Payload is not found // nil is returned. func (n *Network) GetTransactionPayload(transactionRef hash.SHA256Hash) ([]byte, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } transaction, err := n.state.GetTransaction(context.Background(), transactionRef) if err != nil { if errors.Is(err, dag.ErrTransactionNotFound) { @@ -628,11 +651,17 @@ func (n *Network) GetTransactionPayload(transactionRef hash.SHA256Hash) ([]byte, // ListTransactionsInRange returns all transactions known to this Network instance with lamport clock value between startInclusive and endExclusive. func (n *Network) ListTransactionsInRange(startInclusive uint32, endExclusive uint32) ([]dag.Transaction, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } return n.state.FindBetweenLC(context.Background(), startInclusive, endExclusive) } // CreateTransaction creates a new transaction from the given template. func (n *Network) CreateTransaction(ctx context.Context, template Template) (dag.Transaction, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } payloadHash := hash.SHA256Sum(template.Payload) log.Logger(). WithField(core.LogFieldTransactionType, template.Type). @@ -743,6 +772,9 @@ func (n *Network) calculateLamportClock(ctx context.Context, prevs []hash.SHA256 // Shutdown cleans up any leftover go routines func (n *Network) Shutdown() error { + if n.disabled { + return nil + } // Stop protocols and connection manager for _, prot := range n.protocols { prot.Stop() @@ -758,6 +790,9 @@ func (n *Network) Shutdown() error { // Diagnostics collects and returns diagnostics for the Network engine. func (n *Network) Diagnostics() []core.DiagnosticResult { + if n.disabled { + return nil + } var results = make([]core.DiagnosticResult, 0) // Connection manager and protocols results = append(results, core.DiagnosticResultMap{Title: "connections", Items: n.connectionManager.Diagnostics()}) @@ -778,6 +813,9 @@ func (n *Network) Diagnostics() []core.DiagnosticResult { // PeerDiagnostics returns a map containing diagnostic information of the node's peers. The key contains the remote peer's ID. func (n *Network) PeerDiagnostics() map[transport.PeerID]transport.Diagnostics { + if n.disabled { + return nil + } result := make(map[transport.PeerID]transport.Diagnostics, 0) // We assume higher protocol versions (later in the slice) have better/more accurate diagnostics, // so for now they're copied over diagnostics of earlier versions, unless the entry is empty for that peer. @@ -793,15 +831,25 @@ func (n *Network) PeerDiagnostics() map[transport.PeerID]transport.Diagnostics { } func (n *Network) AddressBook() []transport.Contact { + if n.disabled { + return nil + } return n.connectionManager.Contacts() } +func (n *Network) Disabled() bool { + return n.disabled +} + // ReprocessReport describes the reprocess exection. type ReprocessReport struct { // reserved for future use } func (n *Network) Reprocess(ctx context.Context, contentType string) (*ReprocessReport, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } log.Logger().Infof("Starting reprocess of %s", contentType) _, js, err := n.eventPublisher.Pool().Acquire(ctx) diff --git a/network/network_test.go b/network/network_test.go index 9a41c23c5..e07fed42a 100644 --- a/network/network_test.go +++ b/network/network_test.go @@ -261,14 +261,13 @@ func TestNetwork_Configure(t *testing.T) { assert.EqualError(t, err, "disabling TLS in strict mode is not allowed") }) - t.Run("ok - TLS disabled in strict mode, with empty node", func(t *testing.T) { + t.Run("ok - network disabled if not in supported"+ + " DIDMethods", func(t *testing.T) { ctrl := gomock.NewController(t) ctx := createNetwork(t, ctrl) - ctx.protocol.EXPECT().Configure(gomock.Any()) - ctx.network.connectionManager = nil err := ctx.network.Configure(core.TestServerConfig(func(config *core.ServerConfig) { - config.Datadir = io.TestDirectory(t) + config.DIDMethods = []string{"web"} })) require.NoError(t, err) diff --git a/vcr/test.go b/vcr/test.go index 52062bee7..c67fbffd6 100644 --- a/vcr/test.go +++ b/vcr/test.go @@ -177,6 +177,7 @@ func newMockContext(t *testing.T) mockContext { tx.EXPECT().Subscribe("vcr_vcs", gomock.Any(), gomock.Any()) tx.EXPECT().Subscribe("vcr_revocations", gomock.Any(), gomock.Any()) tx.EXPECT().CleanupSubscriberEvents("vcr_vcs", gomock.Any()) + tx.EXPECT().Disabled().AnyTimes() didResolver := resolver.NewMockDIDResolver(ctrl) documentOwner := didsubject.NewMockDocumentOwner(ctrl) vdrInstance := vdr.NewMockVDR(ctrl) diff --git a/vcr/vcr.go b/vcr/vcr.go index 59400e5e0..ac4df93d2 100644 --- a/vcr/vcr.go +++ b/vcr/vcr.go @@ -203,7 +203,6 @@ func (c *vcr) Configure(config core.ServerConfig) error { c.keyResolver = resolver.DIDKeyResolver{Resolver: didResolver} c.serviceResolver = resolver.DIDServiceResolver{Resolver: didResolver} - networkPublisher := issuer.NewNetworkPublisher(c.network, didResolver, c.keyStore) tlsConfig, err := c.pkiProvider.CreateTLSConfig(config.TLS) // returns nil if TLS is disabled if err != nil { return err @@ -225,11 +224,18 @@ func (c *vcr) Configure(config core.ServerConfig) error { c.openidSessionStore = c.storageClient.GetSessionDatabase() } + var networkPublisher issuer.Publisher + if !c.network.Disabled() { + networkPublisher = issuer.NewNetworkPublisher(c.network, didResolver, c.keyStore) + } + status := revocation.NewStatusList2021(c.storageClient.GetSQLDatabase(), client.NewWithCache(config.HTTPClient.Timeout), config.URL) c.issuer = issuer.NewIssuer(c.issuerStore, c, networkPublisher, openidHandlerFn, didResolver, c.keyStore, c.jsonldManager, c.trustConfig, status) c.verifier = verifier.NewVerifier(c.verifierStore, didResolver, c.keyResolver, c.jsonldManager, c.trustConfig, status) - c.ambassador = NewAmbassador(c.network, c, c.verifier, c.eventManager) + if !c.network.Disabled() { + c.ambassador = NewAmbassador(c.network, c, c.verifier, c.eventManager) + } // Create holder/wallet c.wallet = holder.NewSQLWallet(c.keyResolver, c.keyStore, c.verifier, c.jsonldManager, c.storageClient) @@ -269,6 +275,9 @@ func (c *vcr) createCredentialsStore() error { } func (c *vcr) Start() error { + if c.ambassador == nil { // did:nuts / network layer is disabled + return nil + } // start listening for new credentials _ = c.ambassador.Configure() diff --git a/vcr/vcr_test.go b/vcr/vcr_test.go index 2669ecf5f..195b99ccd 100644 --- a/vcr/vcr_test.go +++ b/vcr/vcr_test.go @@ -73,7 +73,9 @@ func TestVCR_Configure(t *testing.T) { vdrInstance.EXPECT().Resolver().AnyTimes() pkiProvider := pki.NewMockProvider(ctrl) pkiProvider.EXPECT().CreateTLSConfig(gomock.Any()).Return(nil, nil).AnyTimes() - instance := NewVCRInstance(nil, vdrInstance, nil, jsonld.NewTestJSONLDManager(t), nil, storage.NewTestStorageEngine(t), pkiProvider).(*vcr) + networkInstance := network.NewMockTransactions(ctrl) + networkInstance.EXPECT().Disabled().AnyTimes() + instance := NewVCRInstance(nil, vdrInstance, networkInstance, jsonld.NewTestJSONLDManager(t), nil, storage.NewTestStorageEngine(t), pkiProvider).(*vcr) instance.config.OpenID4VCI.Enabled = true err := instance.Configure(core.TestServerConfig(func(config *core.ServerConfig) { diff --git a/vdr/vdr.go b/vdr/vdr.go index 22bea69d8..cff3e50f6 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -151,7 +151,10 @@ func (r *Module) Configure(config core.ServerConfig) error { } } - r.networkAmbassador = didnuts.NewAmbassador(r.network, r.store, r.eventManager) + // only create ambassador when did:nuts is enabled + if slices.Contains(r.supportedDIDMethods, "nuts") { + r.networkAmbassador = didnuts.NewAmbassador(r.network, r.store, r.eventManager) + } db := r.storageInstance.GetSQLDatabase() r.didResolver.(*resolver.DIDResolverRouter).Register(didjwk.MethodName, didjwk.NewResolver()) @@ -202,10 +205,17 @@ func (r *Module) Configure(config core.ServerConfig) error { r.Manager = didsubject.New(db, methodManagers, r.keyStore, r.supportedDIDMethods) // Initiate the routines for auto-updating the data. - return r.networkAmbassador.Configure() + if r.networkAmbassador != nil { + return r.networkAmbassador.Configure() + } + return nil } func (r *Module) Start() error { + // nothing to start if did:nuts is disabled + if r.networkAmbassador == nil { + return nil + } err := r.networkAmbassador.Start() if err != nil { return err From b5c5c2dafe72f51912c0c838318027fc5eef5ebf Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 8 Oct 2024 11:02:22 +0200 Subject: [PATCH 26/72] fix configure for discovery service (#3460) --- discovery/module.go | 19 +++++++++++++------ discovery/module_test.go | 11 ++++++++++- policy/local.go | 1 - 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/discovery/module.go b/discovery/module.go index aec286b93..c39cc75d8 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -97,6 +97,19 @@ type Module struct { } func (m *Module) Configure(serverConfig core.ServerConfig) error { + var err error + m.publicURL, err = serverConfig.ServerURL() + if err != nil { + return err + } + + m.httpClient = client.New(serverConfig.Strictmode, serverConfig.HTTPClient.Timeout, nil) + + return m.loadDefinitions() + +} + +func (m *Module) loadDefinitions() error { if m.config.Definitions.Directory == "" { return nil } @@ -110,11 +123,6 @@ func (m *Module) Configure(serverConfig core.ServerConfig) error { return fmt.Errorf("failed to load discovery defintions: %w", err) } - m.publicURL, err = serverConfig.ServerURL() - if err != nil { - return err - } - m.allDefinitions, err = loadDefinitions(m.config.Definitions.Directory) if err != nil { return err @@ -131,7 +139,6 @@ func (m *Module) Configure(serverConfig core.ServerConfig) error { } m.serverDefinitions = serverDefinitions } - m.httpClient = client.New(serverConfig.Strictmode, serverConfig.HTTPClient.Timeout, nil) return nil } diff --git a/discovery/module_test.go b/discovery/module_test.go index 1005bbb8e..4e781ef78 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -405,6 +405,15 @@ func TestModule_Configure(t *testing.T) { _, err := loadDefinitions(config.Definitions.Directory) assert.ErrorContains(t, err, "unable to read definitions directory 'test/non_existent'") }) + t.Run("missing definitions directory", func(t *testing.T) { + config := Config{} + m := &Module{config: config} + err := m.Configure(serverConfig) + + require.NoError(t, err) + assert.NotNil(t, m.publicURL) + assert.NotNil(t, m.httpClient) + }) } func TestModule_Search(t *testing.T) { @@ -513,7 +522,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { subject := make([]credential.DiscoveryRegistrationCredentialSubject, 0) _ = credentials[1].UnmarshalCredentialSubject(&subject) assert.Equal(t, "value", subject[0]["test"]) - assert.Equal(t, "https://example.com/oauth2/alice", subject[0]["authServerURL"]) + assert.Equal(t, "https://nuts.nl/oauth2/alice", subject[0]["authServerURL"]) return &vpAlice, nil }) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) diff --git a/policy/local.go b/policy/local.go index eb18c7399..81719eb7d 100644 --- a/policy/local.go +++ b/policy/local.go @@ -51,7 +51,6 @@ func (b *LocalPDP) Name() string { } func (b *LocalPDP) Configure(_ core.ServerConfig) error { - // check if directory exists if b.config.Directory != "" { _, err := os.Stat(b.config.Directory) if err != nil { From d2df24f05af4989918a20e54d57cdc9c8f8a0531 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 8 Oct 2024 11:51:36 +0200 Subject: [PATCH 27/72] stop refreshing when a subject has no active DID Documents (#3461) --- cmd/root.go | 2 +- discovery/client.go | 24 ++- discovery/client_test.go | 377 +++++++++++++++++---------------------- discovery/module.go | 7 +- discovery/module_test.go | 20 ++- 5 files changed, 208 insertions(+), 222 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index ff1a64421..708876377 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -198,7 +198,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { vdrInstance := vdr.NewVDR(cryptoInstance, networkInstance, didStore, eventManager, storageInstance) credentialInstance := vcr.NewVCRInstance(cryptoInstance, vdrInstance, networkInstance, jsonld, eventManager, storageInstance, pkiInstance) didmanInstance := didman.NewDidmanInstance(vdrInstance, credentialInstance, jsonld) - discoveryInstance := discovery.New(storageInstance, credentialInstance, vdrInstance) + discoveryInstance := discovery.New(storageInstance, credentialInstance, vdrInstance, vdrInstance) authInstance := auth.NewAuthInstance(auth.DefaultConfig(), vdrInstance, vdrInstance, credentialInstance, cryptoInstance, didmanInstance, jsonld, pkiInstance) statusEngine := status.NewStatusEngine(system) metricsEngine := core.NewMetricsEngine() diff --git a/discovery/client.go b/discovery/client.go index 6b4a7b4d1..11a4665ce 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -34,6 +34,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "slices" "strings" "time" @@ -56,15 +57,17 @@ type defaultClientRegistrationManager struct { client client.HTTPClient vcr vcr.VCR subjectManager didsubject.Manager + didResolver resolver.DIDResolver } -func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager) *defaultClientRegistrationManager { +func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver) *defaultClientRegistrationManager { return &defaultClientRegistrationManager{ services: services, store: store, client: client, vcr: vcr, subjectManager: subjectManager, + didResolver: didResolver, } } @@ -87,9 +90,26 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service } } subjectDIDs = subjectDIDs[:j] + + if len(subjectDIDs) == 0 { + return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrDIDMethodsNotSupported, subjectID) + } } + + // and filter by deactivated status + j := 0 + for i, did := range subjectDIDs { + _, _, err := r.didResolver.Resolve(did, nil) + // any temporary error, like db errors should not cause a deregister action, only ErrDeactivated + if err == nil || !errors.Is(err, resolver.ErrDeactivated) { + subjectDIDs[j] = subjectDIDs[i] + j++ + } + } + subjectDIDs = subjectDIDs[:j] + if len(subjectDIDs) == 0 { - return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrDIDMethodsNotSupported, subjectID) + return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, didsubject.ErrSubjectNotFound, subjectID) } log.Logger().Debugf("Registering Verifiable Presentation on Discovery Service (service=%s, subject=%s)", service.ID, subjectID) diff --git a/discovery/client_test.go b/discovery/client_test.go index 76e147d1a..4098df696 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -32,6 +32,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -41,17 +42,53 @@ import ( var nextRefresh = time.Now().Add(-1 * time.Hour) +type testContext struct { + ctrl *gomock.Controller + didResolver *resolver.MockDIDResolver + invoker *client.MockHTTPClient + vcr *vcr.MockVCR + wallet *holder.MockWallet + subjectManager *didsubject.MockManager + store *sqlStore + manager *defaultClientRegistrationManager +} + +func newTestContext(t *testing.T) testContext { + t.Helper() + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + ctrl := gomock.NewController(t) + didResolver := resolver.NewMockDIDResolver(ctrl) + invoker := client.NewMockHTTPClient(ctrl) + vcr := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + subjectManager := didsubject.NewMockManager(ctrl) + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, vcr, subjectManager, didResolver) + vcr.EXPECT().Wallet().Return(wallet).AnyTimes() + + return testContext{ + ctrl: ctrl, + didResolver: didResolver, + invoker: invoker, + vcr: vcr, + wallet: wallet, + subjectManager: subjectManager, + store: store, + manager: manager, + } +} + func Test_defaultClientRegistrationManager_activate(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) t.Run("immediate registration", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, options holder.PresentationOptions, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, options holder.PresentationOptions, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { // check if two credentials are given // check if the DiscoveryRegistrationCredential is added with an authServerURL assert.Len(t, credentials, 2) @@ -61,89 +98,65 @@ func Test_defaultClientRegistrationManager_activate(t *testing.T) { assert.Equal(t, aliceDID.String(), options.Holder.String()) return &vpAlice, nil }) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) assert.NoError(t, err) }) t.Run("registration fails", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("invoker error")) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("invoker error")) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) assert.ErrorContains(t, err, "invoker error") // check no refresh records are added - record, err := store.getPresentationRefreshRecord(testServiceID, aliceSubject) + record, err := ctx.store.getPresentationRefreshRecord(testServiceID, aliceSubject) require.NoError(t, err) assert.Nil(t, record) }) t.Run("DID method not supported", func(t *testing.T) { - ctrl := gomock.NewController(t) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - manager := newRegistrationManager(testDefinitions(), nil, nil, nil, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) assert.ErrorIs(t, err, ErrDIDMethodsNotSupported) }) t.Run("no matching credentials", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorIs(t, err, pe.ErrNoCredentials) }) t.Run("subject with 2 DIDs, one registers and other fails", func(t *testing.T) { + ctx := newTestContext(t) subjectDIDs := []did.DID{aliceDID, bobDID} - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) - wallet := holder.NewMockWallet(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(subjectDIDs, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.didResolver.EXPECT().Resolve(bobDID, gomock.Any()).Return(nil, nil, nil) + ctx.invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(subjectDIDs, nil) // aliceDID registers - wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) + ctx.wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) // bobDID has no credentials, so builds no presentation - wallet.EXPECT().List(gomock.Any(), bobDID).Return(nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), bobDID).Return(nil, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) assert.NoError(t, err) }) @@ -158,48 +171,34 @@ func Test_defaultClientRegistrationManager_activate(t *testing.T) { PresentationMaxValidity: int((24 * time.Hour).Seconds()), }, } - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) - mockVCR := vcr.NewMockVCR(ctrl) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, _ interface{}, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, _ interface{}, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { // expect registration credential assert.Len(t, credentials, 1) return &vpAlice, nil }) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(emptyDefinition, store, invoker, mockVCR, mockSubjectManager) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.manager = newRegistrationManager(emptyDefinition, ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) assert.NoError(t, err) }) t.Run("unknown service", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, nil) + ctx := newTestContext(t) - err := manager.activate(audit.TestContext(), "unknown", aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), "unknown", aliceSubject, nil) assert.EqualError(t, err, "discovery service not found") }) t.Run("unknown subject", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) }) @@ -210,114 +209,74 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { require.NoError(t, storageEngine.Start()) t.Run("not registered", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) t.Run("registered", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - require.NoError(t, store.add(testServiceID, vpAlice, 1)) + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) t.Run("already deactivated", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(holder.NewMockWallet(ctrl)).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) vpAliceDeactivated := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { claims[jwt.AudienceKey] = []string{testServiceID} claims["retract_jti"] = vpAlice.ID.String() vp.Type = append(vp.Type, retractionPresentationType) }, vcAlice) - require.NoError(t, store.add(testServiceID, vpAliceDeactivated, 1)) + require.NoError(t, ctx.store.add(testServiceID, vpAliceDeactivated, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) t.Run("DID method not supported", func(t *testing.T) { - ctrl := gomock.NewController(t) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, nil, nil, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.deactivate(audit.TestContext(), unsupportedServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), unsupportedServiceID, aliceSubject) assert.ErrorIs(t, err, ErrDIDMethodsNotSupported) }) t.Run("deregistering from Discovery Service fails", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - require.NoError(t, store.add(testServiceID, vpAlice, 1)) + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "remote error") }) t.Run("building presentation fails", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil, assert.AnError) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - require.NoError(t, store.add(testServiceID, vpAlice, 1)) + ctx := newTestContext(t) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil, assert.AnError) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.ErrorIs(t, err, assert.AnError) }) t.Run("unknown subject", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) }) @@ -328,109 +287,95 @@ func Test_defaultClientRegistrationManager_refresh(t *testing.T) { require.NoError(t, storageEngine.Start()) t.Run("no registrations", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) require.NoError(t, err) }) t.Run("2 VPs to refresh, first one fails, second one succeeds", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) + ctx := newTestContext(t) gomock.InOrder( - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")), + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")), + ) + gomock.InOrder( + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil), + ctx.didResolver.EXPECT().Resolve(bobDID, gomock.Any()).Return(nil, nil, nil), ) - wallet := holder.NewMockWallet(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) // Alice - _ = store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) - wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) + ctx.wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) // Bob - _ = store.updatePresentationRefreshTime(testServiceID, bobSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), bobSubject).Return([]did.DID{bobDID}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &bobDID, false).Return(&vpBob, nil) - wallet.EXPECT().List(gomock.Any(), bobDID).Return([]vc.VerifiableCredential{vcBob}, nil) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, bobSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), bobSubject).Return([]did.DID{bobDID}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &bobDID, false).Return(&vpBob, nil) + ctx.wallet.EXPECT().List(gomock.Any(), bobDID).Return([]vc.VerifiableCredential{vcBob}, nil) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) errStr := "failed to refresh Verifiable Presentation (service=usecase_v1, subject=bob): registration of Verifiable Presentation on remote Discovery Service failed: did:example:bob: remote error" assert.EqualError(t, err, errStr) // check for presentationRefreshError - refreshError := getPresentationRefreshError(t, store.db, testServiceID, bobSubject) + refreshError := getPresentationRefreshError(t, ctx.store.db, testServiceID, bobSubject) assert.Contains(t, refreshError.Error, errStr) }) t.Run("deactivate unknown subject", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(nil, didsubject.ErrSubjectNotFound) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - _ = store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &nextRefresh) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(nil, didsubject.ErrSubjectNotFound) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &nextRefresh) + + err := ctx.manager.refresh(audit.TestContext(), time.Now()) + + assert.EqualError(t, err, "removed unknown subject (service=usecase_v1, subject=alice)") + }) + t.Run("deactivate deactivated DID", func(t *testing.T) { + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, resolver.ErrDeactivated) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &nextRefresh) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) assert.EqualError(t, err, "removed unknown subject (service=usecase_v1, subject=alice)") }) t.Run("deactivate unsupported DID method", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - _ = store.updatePresentationRefreshTime(unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + _ = ctx.store.updatePresentationRefreshTime(unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) // refresh clears the registration require.NoError(t, err) - record, err := store.getPresentationRefreshRecord(unsupportedServiceID, aliceSubject) + record, err := ctx.store.getPresentationRefreshRecord(unsupportedServiceID, aliceSubject) assert.NoError(t, err) assert.Nil(t, record) }) t.Run("remove presentationRefreshError on success", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) + ctx := newTestContext(t) gomock.InOrder( - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), ) - wallet := holder.NewMockWallet(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) // Alice - _ = store.setPresentationRefreshError(testServiceID, aliceSubject, assert.AnError) - _ = store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &time.Time{}) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) - wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) + _ = ctx.store.setPresentationRefreshError(testServiceID, aliceSubject, assert.AnError) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &time.Time{}) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) + ctx.wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) require.NoError(t, err) // check for presentationRefreshError - refreshError := getPresentationRefreshError(t, store.db, testServiceID, aliceSubject) + refreshError := getPresentationRefreshError(t, ctx.store.db, testServiceID, aliceSubject) assert.Nil(t, refreshError) }) } diff --git a/discovery/module.go b/discovery/module.go index c39cc75d8..0ad5f38e5 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -32,6 +32,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/url" "os" "path" @@ -67,11 +68,12 @@ var _ Client = &Module{} var retractionPresentationType = ssi.MustParseURI("RetractedVerifiablePresentation") // New creates a new Module. -func New(storageInstance storage.Engine, vcrInstance vcr.VCR, subjectManager didsubject.Manager) *Module { +func New(storageInstance storage.Engine, vcrInstance vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver) *Module { m := &Module{ storageInstance: storageInstance, vcrInstance: vcrInstance, subjectManager: subjectManager, + didResolver: didResolver, } m.ctx, m.cancel = context.WithCancel(context.Background()) m.routines = new(sync.WaitGroup) @@ -89,6 +91,7 @@ type Module struct { allDefinitions map[string]ServiceDefinition vcrInstance vcr.VCR subjectManager didsubject.Manager + didResolver resolver.DIDResolver clientUpdater *clientUpdater ctx context.Context cancel context.CancelFunc @@ -149,7 +152,7 @@ func (m *Module) Start() error { return err } m.clientUpdater = newClientUpdater(m.allDefinitions, m.store, m.verifyRegistration, m.httpClient) - m.registrationManager = newRegistrationManager(m.allDefinitions, m.store, m.httpClient, m.vcrInstance, m.subjectManager) + m.registrationManager = newRegistrationManager(m.allDefinitions, m.store, m.httpClient, m.vcrInstance, m.subjectManager, m.didResolver) if m.config.Client.RefreshInterval > 0 { m.routines.Add(1) go func() { diff --git a/discovery/module_test.go b/discovery/module_test.go index 4e781ef78..8e7d317bc 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -35,6 +35,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vcr/verifier" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -309,6 +310,7 @@ type mockContext struct { ctrl *gomock.Controller subjectManager *didsubject.MockManager verifier *verifier.MockVerifier + didResolver *resolver.MockDIDResolver } func setupModule(t *testing.T, storageInstance storage.Engine, visitors ...func(module *Module)) (*Module, mockContext) { @@ -318,7 +320,8 @@ func setupModule(t *testing.T, storageInstance storage.Engine, visitors ...func( mockVCR := vcr.NewMockVCR(ctrl) mockVCR.EXPECT().Verifier().Return(mockVerifier).AnyTimes() mockSubjectManager := didsubject.NewMockManager(ctrl) - m := New(storageInstance, mockVCR, mockSubjectManager) + mockDIDResolver := resolver.NewMockDIDResolver(ctrl) + m := New(storageInstance, mockVCR, mockSubjectManager, mockDIDResolver) m.config = DefaultConfig() m.publicURL = test.MustParseURL("https://example.com") require.NoError(t, m.Configure(core.TestServerConfig())) @@ -344,6 +347,7 @@ func setupModule(t *testing.T, storageInstance storage.Engine, visitors ...func( ctrl: ctrl, verifier: mockVerifier, subjectManager: mockSubjectManager, + didResolver: mockDIDResolver, } } @@ -494,6 +498,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&vpAlice, nil) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, nil) @@ -526,6 +531,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { return &vpAlice, nil }) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, map[string]interface{}{"test": "value"}) @@ -541,6 +547,17 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { require.EqualError(t, err, "subject not found") }) + t.Run("deactivated", func(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + m, testContext := setupModule(t, storageEngine) + testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, resolver.ErrDeactivated) + + err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, nil) + + assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) + }) t.Run("ok, but couldn't register presentation -> maps to ErrRegistrationFailed", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) @@ -549,6 +566,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { m.vcrInstance.(*vcr.MockVCR).EXPECT().Wallet().Return(wallet).MinTimes(1) wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed")).MinTimes(1) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, nil) From b887163fcb29fba73f2f6a7018d76d1cfab775bc Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:13:16 +0200 Subject: [PATCH 28/72] introspection request returns error if missing a token (#3465) * return 415 if using the wrong Content-Type --- auth/api/iam/api.go | 15 ++++++++++++--- auth/api/iam/api_test.go | 34 +++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index a0986f25d..1335973fb 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -30,6 +30,7 @@ import ( "html/template" "net/http" "net/url" + "slices" "strings" "time" @@ -335,7 +336,11 @@ func (r Wrapper) RetrieveAccessToken(_ context.Context, request RetrieveAccessTo } // IntrospectAccessToken allows the resource server (XIS/EHR) to introspect details of an access token issued by this node -func (r Wrapper) IntrospectAccessToken(_ context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) { +func (r Wrapper) IntrospectAccessToken(ctx context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) { + headers := ctx.Value(httpRequestContextKey{}).(*http.Request).Header + if !slices.Contains(headers["Content-Type"], "application/x-www-form-urlencoded") { + return nil, core.Error(http.StatusUnsupportedMediaType, "Content-Type MUST be set to application/x-www-form-urlencoded") + } input := request.Body.Token response, err := r.introspectAccessToken(input) if err != nil { @@ -351,7 +356,11 @@ func (r Wrapper) IntrospectAccessToken(_ context.Context, request IntrospectAcce // IntrospectAccessTokenExtended allows the resource server (XIS/EHR) to introspect details of an access token issued by this node. // It returns the same information as IntrospectAccessToken, but with additional information. -func (r Wrapper) IntrospectAccessTokenExtended(_ context.Context, request IntrospectAccessTokenExtendedRequestObject) (IntrospectAccessTokenExtendedResponseObject, error) { +func (r Wrapper) IntrospectAccessTokenExtended(ctx context.Context, request IntrospectAccessTokenExtendedRequestObject) (IntrospectAccessTokenExtendedResponseObject, error) { + headers := ctx.Value(httpRequestContextKey{}).(*http.Request).Header + if !slices.Contains(headers["Content-Type"], "application/x-www-form-urlencoded") { + return nil, core.Error(http.StatusUnsupportedMediaType, "Content-Type MUST be set to application/x-www-form-urlencoded") + } input := request.Body.Token response, err := r.introspectAccessToken(input) if err != nil { @@ -366,7 +375,7 @@ func (r Wrapper) introspectAccessToken(input string) (*ExtendedTokenIntrospectio // Validate token if input == "" { // Return 200 + 'Active = false' when token is invalid or malformed - log.Logger().Debug("IntrospectAccessToken: missing token") + log.Logger().Warn("IntrospectAccessToken: missing token") return nil, nil } diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 32bc20475..8e3ae8dbc 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -626,21 +626,24 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) { ctx := newTestClient(t) dpopToken, _, thumbprint := newSignedTestDPoP() + req := http.Request{Header: map[string][]string{"Content-Type": {"application/x-www-form-urlencoded"}}} + reqCtx := context.WithValue(context.Background(), httpRequestContextKey{}, &req) + // validate all fields are there after introspection t.Run("error - no token provided", func(t *testing.T) { - res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: ""}}) + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: ""}}) require.NoError(t, err) assert.Equal(t, res, IntrospectAccessToken200JSONResponse{}) }) t.Run("error - other store error", func(t *testing.T) { // token is invalid JSON require.NoError(t, ctx.client.accessTokenServerStore().Put("err", "{")) - res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "err"}}) + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "err"}}) assert.ErrorContains(t, err, "json: cannot unmarshal") assert.Nil(t, res) }) t.Run("error - does not exist", func(t *testing.T) { - res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "does not exist"}}) + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "does not exist"}}) require.NoError(t, err) assert.Equal(t, res, IntrospectAccessToken200JSONResponse{}) }) @@ -648,7 +651,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) { token := AccessToken{Expiration: time.Now().Add(-time.Second)} require.NoError(t, ctx.client.accessTokenServerStore().Put("token", token)) - res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) require.NoError(t, err) assert.Equal(t, res, IntrospectAccessToken200JSONResponse{}) @@ -670,7 +673,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) { token := okToken require.NoError(t, ctx.client.accessTokenServerStore().Put("token", token)) - res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) require.NoError(t, err) tokenResponse, ok := res.(IntrospectAccessToken200JSONResponse) @@ -684,7 +687,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) { token := okToken require.NoError(t, ctx.client.accessTokenServerStore().Put("token", token)) - res, err := ctx.client.IntrospectAccessTokenExtended(context.Background(), IntrospectAccessTokenExtendedRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) + res, err := ctx.client.IntrospectAccessTokenExtended(reqCtx, IntrospectAccessTokenExtendedRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) require.NoError(t, err) tokenResponse, ok := res.(IntrospectAccessTokenExtended200JSONResponse) @@ -703,7 +706,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) { } require.NoError(t, ctx.client.accessTokenServerStore().Put("token", token)) - res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) require.NoError(t, err) tokenResponse, ok := res.(IntrospectAccessToken200JSONResponse) @@ -719,7 +722,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) { } require.NoError(t, ctx.client.accessTokenServerStore().Put("token", token)) - res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}}) require.EqualError(t, err, "IntrospectAccessToken: InputDescriptorConstraintIdMap contains reserved claim name: iss") require.Nil(t, res) @@ -767,13 +770,26 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) { }) require.NoError(t, err) - res, err := ctx.client.IntrospectAccessTokenExtended(context.Background(), IntrospectAccessTokenExtendedRequestObject{Body: &TokenIntrospectionRequest{Token: token.Token}}) + res, err := ctx.client.IntrospectAccessTokenExtended(reqCtx, IntrospectAccessTokenExtendedRequestObject{Body: &TokenIntrospectionRequest{Token: token.Token}}) require.NoError(t, err) tokenResponse, err := json.Marshal(res) assert.NoError(t, err) assert.JSONEq(t, string(expectedResponse), string(tokenResponse)) }) + t.Run("error - wrong Content-Type header", func(t *testing.T) { + req := http.Request{Header: map[string][]string{"Content-Type": {"something-else"}}} + reqCtx := context.WithValue(context.Background(), httpRequestContextKey{}, &req) + expectedErr := core.Error(http.StatusUnsupportedMediaType, "Content-Type MUST be set to application/x-www-form-urlencoded") + + res, err := ctx.client.IntrospectAccessToken(reqCtx, IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "not-empty"}}) + assert.ErrorIs(t, err, expectedErr) + assert.Nil(t, res) + + resExt, err := ctx.client.IntrospectAccessTokenExtended(reqCtx, IntrospectAccessTokenExtendedRequestObject{Body: &TokenIntrospectionRequest{Token: "not-empty"}}) + assert.ErrorIs(t, err, expectedErr) + assert.Nil(t, resExt) + }) } func TestWrapper_Routes(t *testing.T) { From b218f1d195624536169173656cdd8f65bc330890 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 08:37:40 +0200 Subject: [PATCH 29/72] Bump google.golang.org/protobuf from 1.34.2 to 1.35.1 (#3466) * Bump google.golang.org/protobuf from 1.34.2 to 1.35.1 Bumps google.golang.org/protobuf from 1.34.2 to 1.35.1. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * regen protobuf --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gerard Snaauw --- go.mod | 2 +- go.sum | 4 ++-- network/transport/grpc/testprotocol.pb.go | 2 +- network/transport/grpc/testprotocol_grpc.pb.go | 2 +- network/transport/v2/protocol.pb.go | 2 +- network/transport/v2/protocol_grpc.pb.go | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 62ce2c458..f891078dd 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( golang.org/x/crypto v0.28.0 golang.org/x/time v0.7.0 google.golang.org/grpc v1.67.1 - google.golang.org/protobuf v1.34.2 + google.golang.org/protobuf v1.35.1 gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 diff --git a/go.sum b/go.sum index 1e3f26759..9345a8b25 100644 --- a/go.sum +++ b/go.sum @@ -654,8 +654,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 h1:rz88vn1OH2B9kKorR+QCrcuw6WbizVwahU2Y9Q09xqU= gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3/go.mod h1:vJmfdx2L0+30M90zUd0GCjLV14Ip3ZgWR5+MV1qljOo= gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 h1:pSSZonNnrORBQXIm3kl6P9EQTNqVds9zszK/BXbOItg= diff --git a/network/transport/grpc/testprotocol.pb.go b/network/transport/grpc/testprotocol.pb.go index dc9d6f9dc..8ae4e6dfe 100644 --- a/network/transport/grpc/testprotocol.pb.go +++ b/network/transport/grpc/testprotocol.pb.go @@ -18,7 +18,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc v5.28.2 // source: transport/grpc/testprotocol.proto package grpc diff --git a/network/transport/grpc/testprotocol_grpc.pb.go b/network/transport/grpc/testprotocol_grpc.pb.go index 0461d719f..592d7ea81 100644 --- a/network/transport/grpc/testprotocol_grpc.pb.go +++ b/network/transport/grpc/testprotocol_grpc.pb.go @@ -18,7 +18,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v5.28.2 // source: transport/grpc/testprotocol.proto package grpc diff --git a/network/transport/v2/protocol.pb.go b/network/transport/v2/protocol.pb.go index 061eff71c..f3829073f 100644 --- a/network/transport/v2/protocol.pb.go +++ b/network/transport/v2/protocol.pb.go @@ -18,7 +18,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc v5.28.2 // source: transport/v2/protocol.proto package v2 diff --git a/network/transport/v2/protocol_grpc.pb.go b/network/transport/v2/protocol_grpc.pb.go index 56c7a7054..1fa26ffcf 100644 --- a/network/transport/v2/protocol_grpc.pb.go +++ b/network/transport/v2/protocol_grpc.pb.go @@ -18,7 +18,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v5.28.2 // source: transport/v2/protocol.proto package v2 From 793592a934f718dc1a4815f5a4e43ae95d4ba5a0 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:59:51 +0200 Subject: [PATCH 30/72] Update go-stoabs to v1.10.0 (#3469) --- go.mod | 17 ++++------- go.sum | 95 ++++++++-------------------------------------------------- 2 files changed, 18 insertions(+), 94 deletions(-) diff --git a/go.mod b/go.mod index f891078dd..5c450769a 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/providers/posflag v0.1.0 github.com/knadh/koanf/providers/structs v0.1.0 + github.com/knadh/koanf/v2 v2.1.1 github.com/labstack/echo/v4 v4.12.0 github.com/lestrrat-go/jwx/v2 v2.1.1 github.com/magiconair/properties v1.8.7 @@ -32,7 +33,7 @@ require ( github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b github.com/nuts-foundation/go-did v0.14.0 github.com/nuts-foundation/go-leia/v4 v4.0.3 - github.com/nuts-foundation/go-stoabs v1.9.0 + github.com/nuts-foundation/go-stoabs v1.10.0 // check the oapi-codegen tool version in the makefile when upgrading the runtime github.com/oapi-codegen/runtime v1.1.1 github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f @@ -71,7 +72,7 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/PaesslerAG/gval v1.2.2 // indirect github.com/alexandrevicenzi/go-sse v1.6.0 // indirect - github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bwesterb/byteswriter v1.0.0 // indirect @@ -101,6 +102,7 @@ require ( github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redsync/redsync/v4 v4.13.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect @@ -110,8 +112,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -129,7 +129,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.10 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect @@ -187,9 +187,8 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect @@ -203,7 +202,3 @@ require ( modernc.org/sqlite v1.32.0 // indirect rsc.io/qr v0.2.0 // indirect ) - -require github.com/knadh/koanf/v2 v2.1.1 - -require github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect diff --git a/go.sum b/go.sum index 9345a8b25..fc966c9e7 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= @@ -25,7 +24,6 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDm github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E= @@ -36,8 +34,8 @@ github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97/go.mod h1:zT github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/alexandrevicenzi/go-sse v1.6.0 h1:3KvOzpuY7UrbqZgAtOEmub9/V5ykr7Myudw+PA+H1Ik= github.com/alexandrevicenzi/go-sse v1.6.0/go.mod h1:jdrNAhMgVqP7OfcUuM8eJx0sOY17wc+girs5utpFZUU= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= @@ -68,7 +66,6 @@ github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprY github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -79,8 +76,6 @@ github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKa github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -88,10 +83,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4APOQ= -github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgraph-io/badger/v4 v4.3.1 h1:7r5wKqmoRpGgSxqa0S/nGdpOpvvzuREGPLSua73C8tw= +github.com/dgraph-io/badger/v4 v4.3.1/go.mod h1:oObz97DImXpd6O/Dt8BqdKLLTDmEmarAimo72VV5whQ= +github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= +github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= @@ -105,10 +100,6 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/eknkc/basex v1.0.1 h1:TcyAkqh4oJXgV3WYyL4KEfCMk9W8oJCpmx1bo+jVgKY= github.com/eknkc/basex v1.0.1/go.mod h1:k/F/exNEHFdbs3ZHuasoP2E7zeWwZblG84Y7Z59vQRo= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -169,44 +160,21 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/goodsign/monday v1.0.2 h1:k8kRMkCRVfCTWOU4dRfRgneQsWlB1+mJd3MxG0lGLzQ= github.com/goodsign/monday v1.0.2/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -264,8 +232,8 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= @@ -384,8 +352,8 @@ github.com/nuts-foundation/go-did v0.14.0 h1:Y1tuQCC2xmDX1bdXQS9iquwzJgcT1zcJxbZ github.com/nuts-foundation/go-did v0.14.0/go.mod h1:dQm9b2dYUnhgVW1FmpAi5nNe0mfIrnxM3EaQx4GsDhI= github.com/nuts-foundation/go-leia/v4 v4.0.3 h1:xNZznXWvcIwonXIDmpDDvF7KmP9BOK0MFt9ir3RD2gI= github.com/nuts-foundation/go-leia/v4 v4.0.3/go.mod h1:tYveGED8tSbQYhZNv2DVTc51c2zEWmSF+MG96PAtalY= -github.com/nuts-foundation/go-stoabs v1.9.0 h1:zK+ugfolaJYyBvGwsRuavLVdycXk4Yw/1gI+tz17lWQ= -github.com/nuts-foundation/go-stoabs v1.9.0/go.mod h1:htbUqSZiaihqAvJfHwtAbQusGaJtIeWpm1pmKjBYXlM= +github.com/nuts-foundation/go-stoabs v1.10.0 h1:mNzm9jgraMc69a8gTgteli8t1CMxr1+gyI7A9Eh0NDk= +github.com/nuts-foundation/go-stoabs v1.10.0/go.mod h1:So6S7ninucyJUU7I+JK1zcpoGDsZtd+jLXXacVtSWew= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= @@ -418,7 +386,6 @@ github.com/privacybydesign/irmago v0.16.0 h1:PxIPRvpitxfJSocIRwIoYDSETarsopxtByZ github.com/privacybydesign/irmago v0.16.0/go.mod h1:++4PaFrN8MuRkunja/ID6cX+5KcATi7I0uVG+asI96c= github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= @@ -535,25 +502,17 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -562,18 +521,14 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -619,10 +574,6 @@ golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -630,30 +581,10 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 h1:rz88vn1OH2B9kKorR+QCrcuw6WbizVwahU2Y9Q09xqU= @@ -684,8 +615,6 @@ gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= From 3590c1e7ce6c4d1ba1ce24c4097d5e0ab3859b8b Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:55:02 +0200 Subject: [PATCH 31/72] re-enable did:web migration (#3473) --- docs/pages/deployment/migration.rst | 3 +-- .../integrating/version-incompatibilities.rst | 15 +++++++++++++ docs/pages/release_notes.rst | 5 +++-- e2e-tests/migration/main_test.go | 17 +++++--------- vdr/didsubject/manager.go | 22 ++----------------- vdr/didsubject/manager_test.go | 4 +--- vdr/vdr.go | 3 +-- vdr/vdr_test.go | 2 +- 8 files changed, 29 insertions(+), 42 deletions(-) diff --git a/docs/pages/deployment/migration.rst b/docs/pages/deployment/migration.rst index a5f164886..6734b695d 100644 --- a/docs/pages/deployment/migration.rst +++ b/docs/pages/deployment/migration.rst @@ -7,7 +7,7 @@ Nuts node v6 runs several migrations on startup for DID documents that are manag 1. Remove controllers and add self-control to ``did:nuts`` documents, 2. Import ``did:nuts`` documents into the new SQL database under a ``subject`` with the same name, and -3. Add a ``did:web`` document with the same services to the same ``subject``. +3. Add a ``did:web`` document to the same ``subject``. **Migration: convert did:nuts to self-control** Requires ``didmethods`` to contain ``nuts``. @@ -34,6 +34,5 @@ See ``/status/diagnostics`` if you own any DIDs with a document conflict. If so, Requires ``didmethods`` to contain ``web`` and ``nuts`` (default). This migration adds a new ``did:web`` DID Document to owned subjects that do not already have one. -All services from the ``did:nuts`` DID Document are copied to the new document. A new verification method is created for the document and added to all verification relationships except KeyAgreement. This means did:web cannot be used for encryption (yet). diff --git a/docs/pages/integrating/version-incompatibilities.rst b/docs/pages/integrating/version-incompatibilities.rst index 6ab7371be..23e167f96 100644 --- a/docs/pages/integrating/version-incompatibilities.rst +++ b/docs/pages/integrating/version-incompatibilities.rst @@ -16,3 +16,18 @@ There are basically two options. Do not use the VDR V1 and VDR V2 API at the same time. This will lead to unexpected behavior. Once you use the VDR V2 API, you cannot go back to the VDR V1 API. The VDR V1 API has also been marked as deprecated. + +Publishing Services for use-cases +********************************* + +V5 use-cases define service endpoints or a collection of endpoints that should be registered in the Services on DID Documents. +The concrete endpoints are usually on the DID Document of the vendor, and then referenced by all DID Documents managed by that vendor. +And ``did:nuts`` for example, requires the registration of a ``NutsComm`` endpoint to authenticate the connection. +Use-cases built on V5 should keep using the DIDMan API to manage and resolve Services on DID Documents. +Any Service change made using the DIDMan API will only update ``did:nuts`` DID Documents. + +For use-cases built on V6, any endpoint needed for the use-case should be listed in the registration on the Discovery Service for that use-case, see :ref:`discovery` Registration. +This means that ``did:web`` DID Documents (or non-did:nuts if we look further ahead) will contain very few Services, if any. +If there is a need to add a Service for V6 use-cases, they should be added using the VDR v2 API, which will then add the Service to _all_ DIDs that are part of the Subject. +Note that resolving Services using the VDR v2 API will return the Service from the document as is. +So, it resolves Services without following any references in the Service to a concrete endpoint as is done by DIDMan. \ No newline at end of file diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index 240e6d677..3d6eb939e 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -19,6 +19,7 @@ Breaking changes - The VDR v1 ``createDID`` (``POST /internal/vdr/v1/did``) no longer supports the ``controller`` and ``selfControl`` fields. All did:nuts documents are now self controlled. All existing documents will be migrated to self controlled at startup. - Managed ``did:nuts`` DIDs are migrated to the new SQL storage. Unresolved DID document conflicts may contain an incorrect state after migrating to v6. See ``/status/diagnostics`` if you own any DIDs with a document conflict; use ``/internal/vdr/v1/did/conflicted`` to find the specific DIDs. - Removed legacy API authentication tokens. +- See caveats in :ref:`version-incompatibilities`. ============ New Features @@ -52,7 +53,7 @@ Changes - Removed support for the UZI authentication means. - Documentation of ``did:nuts``-related features have been removed (refer to v5 documentation). - Documentation of specific use cases (e.g. health care in general or eOverdracht) has been moved to the `Nuts wiki `_. -- Node can now be run without configuring TLS when the gRPC network isn't used (no bootstrap node configured and no network state), to cater use cases that don't use ``did:nuts``. +- Node can now be run without configuring TLS when the gRPC network isn't used (``didmethods`` does not contain ``nuts``), to cater use cases that don't use ``did:nuts``. - Crypto backends store keys under a key name and are linked to the kid via the ``key_reference`` SQL table. The following features have also been changed: @@ -63,7 +64,7 @@ DID management You no longer manage changes to DIDs but to Subjects. Each subject has multiple DIDs, one for each enabled DID method. You're free to choose an ID for a Subject. This feature enables forwards compatibility with new DID methods. DID methods can be enabled and disabled via the ``didmethods`` config parameter. (Default: ``['web','nuts']``). -Existing ``did:nuts`` documents will be migrated to self-controlled at startup and the DID will be added as SubjectID. +Existing ``did:nuts`` documents will be migrated to self-controlled at startup and the DID will be added as SubjectID together with a new ``did:web`` DID. See :ref:`nuts-node-migrations` for more information. HTTP interface diff --git a/e2e-tests/migration/main_test.go b/e2e-tests/migration/main_test.go index 7532897ec..2875d63dd 100644 --- a/e2e-tests/migration/main_test.go +++ b/e2e-tests/migration/main_test.go @@ -47,7 +47,7 @@ func Test_Migrations(t *testing.T) { DIDs, err := man.DID.All() require.NoError(t, err) - require.Len(t, DIDs, 4) // 4 did:nuts, 3 did:web + require.Len(t, DIDs, 7) // 4 did:nuts, 3 did:web t.Run("vendor", func(t *testing.T) { // versions for did:nuts: @@ -67,7 +67,7 @@ func Test_Migrations(t *testing.T) { assert.Len(t, doc.VerificationMethods, 2) // migration: add did:web - EqualServices(t, man, doc) + hasDIDWeb(t, man, doc) }) t.Run("org1", func(t *testing.T) { // versions for did:nuts: @@ -89,7 +89,7 @@ func Test_Migrations(t *testing.T) { assert.Empty(t, didDoc.Controller) // migration: add did:web - EqualServices(t, man, doc) + hasDIDWeb(t, man, doc) }) t.Run("org2", func(t *testing.T) { // versions for did:nuts: @@ -133,14 +133,11 @@ func Test_Migrations(t *testing.T) { assert.Empty(t, didDoc.Controller) // migration: add did:web - EqualServices(t, man, doc) + hasDIDWeb(t, man, doc) }) } -func EqualServices(t *testing.T, man *manager, nutsDoc *orm.DidDocument) { - return // disable until there is a fix for https://github.com/nuts-foundation/nuts-node/issues/3444 - didWebPrefix := "did:web:nodeA%3A8080" - +func hasDIDWeb(t *testing.T, man *manager, nutsDoc *orm.DidDocument) { dids, err := man.DID.FindBySubject(nutsDoc.DID.Subject) // migrated documents have subject == did:nuts:... require.NoError(t, err) assert.Len(t, dids, 2) @@ -152,8 +149,4 @@ func EqualServices(t *testing.T, man *manager, nutsDoc *orm.DidDocument) { } } assert.Equal(t, 0, webDoc.Version) - assert.Equal(t, len(nutsDoc.Services), len(webDoc.Services)) - for _, service := range webDoc.Services { - assert.True(t, strings.HasPrefix(service.ID, didWebPrefix)) - } } diff --git a/vdr/didsubject/manager.go b/vdr/didsubject/manager.go index a74825a00..2ece1f276 100644 --- a/vdr/didsubject/manager.go +++ b/vdr/didsubject/manager.go @@ -769,26 +769,8 @@ func (r *SqlManager) MigrateAddWebToNuts(ctx context.Context, id did.DID) error ID: webDoc.DID.ID, Subject: subject, } - // rename services. only the DID part of the service.ID needs to be updates - webDoc.Services = make([]orm.Service, len(nutsDoc.Services)) - for i, ormService := range nutsDoc.Services { - service := new(did.Service) - err = json.Unmarshal(ormService.Data, service) - if err != nil { - return err - } - service.ID = ssi.MustParseURI(webDID.ID + "#" + service.ID.Fragment) - rawService, err := json.Marshal(service) - if err != nil { - return err - } - webDoc.Services[i] = orm.Service{ - ID: service.ID.String(), - Data: rawService, - } - } - // store did:web - _, err = sqlDIDDocumentManager.CreateOrUpdate(webDID, webDoc.VerificationMethods, webDoc.Services) + // store did:web; don't migrate services + _, err = sqlDIDDocumentManager.CreateOrUpdate(webDID, webDoc.VerificationMethods, nil) if err != nil { return err } diff --git a/vdr/didsubject/manager_test.go b/vdr/didsubject/manager_test.go index 4e85f92c4..526980bce 100644 --- a/vdr/didsubject/manager_test.go +++ b/vdr/didsubject/manager_test.go @@ -627,12 +627,10 @@ func TestSqlManager_MigrateAddWebToNuts(t *testing.T) { assert.Equal(t, didNuts.String(), dids[0].ID) assert.Equal(t, didWeb.String(), dids[1].ID) - docNuts, err := NewDIDDocumentManager(db).Latest(didNuts, nil) require.NoError(t, err) docWeb, err := NewDIDDocumentManager(db).Latest(didWeb, nil) require.NoError(t, err) - assert.Equal(t, len(docNuts.Services), len(docWeb.Services)) - assert.Equal(t, didWeb.String()+"#service-1", docWeb.Services[0].ID) + assert.Len(t, docWeb.Services, 0) }) t.Run("ok - already has did:web", func(t *testing.T) { db := testDB(t) diff --git a/vdr/vdr.go b/vdr/vdr.go index cff3e50f6..0672bf339 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -392,8 +392,7 @@ func (r *Module) allMigrations() []migration { return []migration{ // key will be printed as description of the migration {r.migrateRemoveControllerFromDIDNuts, "remove controller"}, // must come before migrateHistoryOwnedDIDNuts so controller removal is also migrated. {r.migrateHistoryOwnedDIDNuts, "document history"}, - // Disable migration until we have a fix for: https://github.com/nuts-foundation/nuts-node/issues/3444 - //{r.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}, // must come after migrateHistoryOwnedDIDNuts since it acts on the SQL store. + {r.migrateAddDIDWebToOwnedDIDNuts, "add did:web to subject"}, // must come after migrateHistoryOwnedDIDNuts since it acts on the SQL store. } } diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index 2eebeb9b5..1c57bdc8f 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -348,7 +348,7 @@ func TestVDR_Migrate(t *testing.T) { ctx.mockDocumentOwner.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{testDIDWeb}, nil) err := ctx.vdr.Migrate() assert.NoError(t, err) - assert.Len(t, ctx.vdr.migrations, 2) // confirm its running allMigrations() that currently is only did:nuts + assert.Len(t, ctx.vdr.migrations, 3) // confirm its running allMigrations() that currently is only did:nuts }) t.Run("controller migration", func(t *testing.T) { controllerMigrationSetup := func(t *testing.T) vdrTestCtx { From 869308b3d0672ccaf1bd312973820c974f0b807c Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:15:34 +0200 Subject: [PATCH 32/72] VCR: remove credentials based on subject (#3474) --- docs/_static/vcr/vcr_v2.yaml | 14 +- vcr/api/vcr/v2/api.go | 41 +++-- vcr/api/vcr/v2/api_test.go | 52 ++++-- vcr/api/vcr/v2/generated.go | 330 +++++++++++++++++------------------ 4 files changed, 240 insertions(+), 197 deletions(-) diff --git a/docs/_static/vcr/vcr_v2.yaml b/docs/_static/vcr/vcr_v2.yaml index f6e891d38..64dce67d0 100644 --- a/docs/_static/vcr/vcr_v2.yaml +++ b/docs/_static/vcr/vcr_v2.yaml @@ -530,17 +530,17 @@ paths: description: The credential will not be altered in any way, so no need to return it. default: $ref: '../common/error_response.yaml' - /internal/vcr/v2/holder/{did}/vc/{id}: + /internal/vcr/v2/holder/{subjectID}/vc/{id}: parameters: - - name: did + - name: subjectID in: path - description: URL encoded DID. + description: Subject ID of the wallet owner at this node. required: true content: plain/text: schema: type: string - example: did:web:example.com + example: 90BC1AE9-752B-432F-ADC3-DD9F9C61843C - name: id in: path description: URL encoded VC ID. @@ -552,15 +552,15 @@ paths: description: | Remove a VerifiableCredential from the holders wallet. After removal the holder can't present the credential any more. It does not revoke the credential or inform the credential issuer that the wallet removed the wallet. - + error returns: * 400 - Invalid credential - * 404 - Credential not found + * 404 - Credential or subject not found * 500 - An error occurred while processing the request operationId: removeCredentialFromWallet tags: - - credential + - credential responses: "204": description: Credential has been removed from the wallet. diff --git a/vcr/api/vcr/v2/api.go b/vcr/api/vcr/v2/api.go index 1105838ea..e9f2763da 100644 --- a/vcr/api/vcr/v2/api.go +++ b/vcr/api/vcr/v2/api.go @@ -23,15 +23,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/nuts-foundation/nuts-node/audit" - "github.com/nuts-foundation/nuts-node/jsonld" - "github.com/nuts-foundation/nuts-node/vcr/credential" - "github.com/nuts-foundation/nuts-node/vcr/holder" - "github.com/nuts-foundation/nuts-node/vcr/issuer" - vcrTypes "github.com/nuts-foundation/nuts-node/vcr/types" - "github.com/nuts-foundation/nuts-node/vcr/verifier" - "github.com/nuts-foundation/nuts-node/vdr/didsubject" - "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/http" "strings" "time" @@ -40,9 +31,18 @@ import ( ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/vcr" + "github.com/nuts-foundation/nuts-node/vcr/credential" + "github.com/nuts-foundation/nuts-node/vcr/holder" + "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" + vcrTypes "github.com/nuts-foundation/nuts-node/vcr/types" + "github.com/nuts-foundation/nuts-node/vcr/verifier" + "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" ) var clockFn = func() time.Time { @@ -461,20 +461,31 @@ func (w *Wrapper) GetCredentialsInWallet(ctx context.Context, request GetCredent } func (w *Wrapper) RemoveCredentialFromWallet(ctx context.Context, request RemoveCredentialFromWalletRequestObject) (RemoveCredentialFromWalletResponseObject, error) { - holderDID, err := did.ParseDID(request.Did) + // get DIDs for holder + dids, err := w.SubjectManager.ListDIDs(ctx, request.SubjectID) if err != nil { - return nil, core.InvalidInputError("invalid holder DID: %w", err) + return nil, err } credentialID, err := ssi.ParseURI(request.Id) if err != nil { return nil, core.InvalidInputError("invalid credential ID: %w", err) } - err = w.VCR.Wallet().Remove(ctx, *holderDID, *credentialID) - if err != nil { - return nil, err + var deleted bool + for _, subjectDID := range dids { + err = w.VCR.Wallet().Remove(ctx, subjectDID, *credentialID) + if err != nil { + if errors.Is(err, vcrTypes.ErrNotFound) { + // only return vcrTypes.ErrNotFound if true for all subjectDIDs (deleted=false) + continue + } + return nil, err + } + deleted = true + } + if !deleted { + return nil, vcrTypes.ErrNotFound } return RemoveCredentialFromWallet204Response{}, nil - } // TrustIssuer handles API request to start trusting an issuer of a Verifiable Credential. diff --git a/vcr/api/vcr/v2/api_test.go b/vcr/api/vcr/v2/api_test.go index ce0fcb730..6c6ae24d5 100644 --- a/vcr/api/vcr/v2/api_test.go +++ b/vcr/api/vcr/v2/api_test.go @@ -23,8 +23,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/nuts-foundation/nuts-node/vdr/didsubject" - "github.com/nuts-foundation/nuts-node/vdr/resolver" + "github.com/nuts-foundation/nuts-node/vcr/types" "net/http" "testing" "time" @@ -41,6 +40,8 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vcr/verifier" + "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -827,26 +828,57 @@ func TestWrapper_GetCredentialsInWallet(t *testing.T) { }) } -func TestWrapper_RemoveCredentialFromWallet(t *testing.T) { +func TestWrapper_RemoveCredentialFromSubjectWallet(t *testing.T) { + didNuts := did.MustParseDID("did:nuts:123") + didWeb := did.MustParseDID("did:web:example.com") + subject := "subbie" t.Run("ok", func(t *testing.T) { testContext := newMockContext(t) - testContext.mockWallet.EXPECT().Remove(testContext.requestCtx, holderDID, credentialID).Return(nil) + testContext.mockSubjectManager.EXPECT().ListDIDs(testContext.requestCtx, subject).Return([]did.DID{didNuts, didWeb}, nil) + testContext.mockWallet.EXPECT().Remove(testContext.requestCtx, didNuts, credentialID).Return(nil) + testContext.mockWallet.EXPECT().Remove(testContext.requestCtx, didWeb, credentialID).Return(types.ErrNotFound) // only exists on 1 DID response, err := testContext.client.RemoveCredentialFromWallet(testContext.requestCtx, RemoveCredentialFromWalletRequestObject{ - Did: holderDID.String(), - Id: credentialID.String(), + SubjectID: subject, + Id: credentialID.String(), }) assert.NoError(t, err) assert.Equal(t, RemoveCredentialFromWallet204Response{}, response) }) - t.Run("error", func(t *testing.T) { + t.Run("error - credential not found", func(t *testing.T) { + testContext := newMockContext(t) + testContext.mockSubjectManager.EXPECT().ListDIDs(testContext.requestCtx, subject).Return([]did.DID{didNuts, didWeb}, nil) + testContext.mockWallet.EXPECT().Remove(testContext.requestCtx, gomock.AnyOf(didNuts, didWeb), credentialID).Return(types.ErrNotFound).Times(2) + + response, err := testContext.client.RemoveCredentialFromWallet(testContext.requestCtx, RemoveCredentialFromWalletRequestObject{ + SubjectID: subject, + Id: credentialID.String(), + }) + + assert.Empty(t, response) + assert.ErrorIs(t, err, types.ErrNotFound) + }) + t.Run("error - subject not found", func(t *testing.T) { + testContext := newMockContext(t) + testContext.mockSubjectManager.EXPECT().ListDIDs(testContext.requestCtx, subject).Return(nil, didsubject.ErrSubjectNotFound) + + response, err := testContext.client.RemoveCredentialFromWallet(testContext.requestCtx, RemoveCredentialFromWalletRequestObject{ + SubjectID: subject, + Id: credentialID.String(), + }) + + assert.Empty(t, response) + assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) + }) + t.Run("error - general error", func(t *testing.T) { testContext := newMockContext(t) - testContext.mockWallet.EXPECT().Remove(testContext.requestCtx, holderDID, credentialID).Return(assert.AnError) + testContext.mockSubjectManager.EXPECT().ListDIDs(testContext.requestCtx, subject).Return([]did.DID{didNuts, didWeb}, nil) + testContext.mockWallet.EXPECT().Remove(testContext.requestCtx, didNuts, credentialID).Return(assert.AnError) response, err := testContext.client.RemoveCredentialFromWallet(testContext.requestCtx, RemoveCredentialFromWalletRequestObject{ - Did: holderDID.String(), - Id: credentialID.String(), + SubjectID: subject, + Id: credentialID.String(), }) assert.Empty(t, response) diff --git a/vcr/api/vcr/v2/generated.go b/vcr/api/vcr/v2/generated.go index 19197b89f..3e31b3c92 100644 --- a/vcr/api/vcr/v2/generated.go +++ b/vcr/api/vcr/v2/generated.go @@ -482,9 +482,6 @@ type ClientInterface interface { CreateVP(ctx context.Context, body CreateVPJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // RemoveCredentialFromWallet request - RemoveCredentialFromWallet(ctx context.Context, did string, id string, reqEditors ...RequestEditorFn) (*http.Response, error) - // GetCredentialsInWallet request GetCredentialsInWallet(ctx context.Context, subjectID string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -493,6 +490,9 @@ type ClientInterface interface { LoadVC(ctx context.Context, subjectID string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // RemoveCredentialFromWallet request + RemoveCredentialFromWallet(ctx context.Context, subjectID string, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + // IssueVCWithBody request with any body IssueVCWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -563,8 +563,8 @@ func (c *Client) CreateVP(ctx context.Context, body CreateVPJSONRequestBody, req return c.Client.Do(req) } -func (c *Client) RemoveCredentialFromWallet(ctx context.Context, did string, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewRemoveCredentialFromWalletRequest(c.Server, did, id) +func (c *Client) GetCredentialsInWallet(ctx context.Context, subjectID string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetCredentialsInWalletRequest(c.Server, subjectID) if err != nil { return nil, err } @@ -575,8 +575,8 @@ func (c *Client) RemoveCredentialFromWallet(ctx context.Context, did string, id return c.Client.Do(req) } -func (c *Client) GetCredentialsInWallet(ctx context.Context, subjectID string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetCredentialsInWalletRequest(c.Server, subjectID) +func (c *Client) LoadVCWithBody(ctx context.Context, subjectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLoadVCRequestWithBody(c.Server, subjectID, contentType, body) if err != nil { return nil, err } @@ -587,8 +587,8 @@ func (c *Client) GetCredentialsInWallet(ctx context.Context, subjectID string, r return c.Client.Do(req) } -func (c *Client) LoadVCWithBody(ctx context.Context, subjectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewLoadVCRequestWithBody(c.Server, subjectID, contentType, body) +func (c *Client) LoadVC(ctx context.Context, subjectID string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLoadVCRequest(c.Server, subjectID, body) if err != nil { return nil, err } @@ -599,8 +599,8 @@ func (c *Client) LoadVCWithBody(ctx context.Context, subjectID string, contentTy return c.Client.Do(req) } -func (c *Client) LoadVC(ctx context.Context, subjectID string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewLoadVCRequest(c.Server, subjectID, body) +func (c *Client) RemoveCredentialFromWallet(ctx context.Context, subjectID string, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRemoveCredentialFromWalletRequest(c.Server, subjectID, id) if err != nil { return nil, err } @@ -855,27 +855,20 @@ func NewCreateVPRequestWithBody(server string, contentType string, body io.Reade return req, nil } -// NewRemoveCredentialFromWalletRequest generates requests for RemoveCredentialFromWallet -func NewRemoveCredentialFromWalletRequest(server string, did string, id string) (*http.Request, error) { +// NewGetCredentialsInWalletRequest generates requests for GetCredentialsInWallet +func NewGetCredentialsInWalletRequest(server string, subjectID string) (*http.Request, error) { var err error var pathParam0 string - pathParam0 = did - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) - if err != nil { - return nil, err - } + pathParam0 = subjectID serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/internal/vcr/v2/holder/%s/vc/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/internal/vcr/v2/holder/%s/vc", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -885,7 +878,7 @@ func NewRemoveCredentialFromWalletRequest(server string, did string, id string) return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } @@ -893,8 +886,19 @@ func NewRemoveCredentialFromWalletRequest(server string, did string, id string) return req, nil } -// NewGetCredentialsInWalletRequest generates requests for GetCredentialsInWallet -func NewGetCredentialsInWalletRequest(server string, subjectID string) (*http.Request, error) { +// NewLoadVCRequest calls the generic LoadVC builder with application/json body +func NewLoadVCRequest(server string, subjectID string, body LoadVCJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewLoadVCRequestWithBody(server, subjectID, "application/json", bodyReader) +} + +// NewLoadVCRequestWithBody generates requests for LoadVC with any type of body +func NewLoadVCRequestWithBody(server string, subjectID string, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -916,39 +920,37 @@ func NewGetCredentialsInWalletRequest(server string, subjectID string) (*http.Re return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest("POST", queryURL.String(), body) if err != nil { return nil, err } - return req, nil -} + req.Header.Add("Content-Type", contentType) -// NewLoadVCRequest calls the generic LoadVC builder with application/json body -func NewLoadVCRequest(server string, subjectID string, body LoadVCJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewLoadVCRequestWithBody(server, subjectID, "application/json", bodyReader) + return req, nil } -// NewLoadVCRequestWithBody generates requests for LoadVC with any type of body -func NewLoadVCRequestWithBody(server string, subjectID string, contentType string, body io.Reader) (*http.Request, error) { +// NewRemoveCredentialFromWalletRequest generates requests for RemoveCredentialFromWallet +func NewRemoveCredentialFromWalletRequest(server string, subjectID string, id string) (*http.Request, error) { var err error var pathParam0 string pathParam0 = subjectID + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/internal/vcr/v2/holder/%s/vc", pathParam0) + operationPath := fmt.Sprintf("/internal/vcr/v2/holder/%s/vc/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -958,13 +960,11 @@ func NewLoadVCRequestWithBody(server string, subjectID string, contentType strin return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("DELETE", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } @@ -1465,9 +1465,6 @@ type ClientWithResponsesInterface interface { CreateVPWithResponse(ctx context.Context, body CreateVPJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateVPResponse, error) - // RemoveCredentialFromWalletWithResponse request - RemoveCredentialFromWalletWithResponse(ctx context.Context, did string, id string, reqEditors ...RequestEditorFn) (*RemoveCredentialFromWalletResponse, error) - // GetCredentialsInWalletWithResponse request GetCredentialsInWalletWithResponse(ctx context.Context, subjectID string, reqEditors ...RequestEditorFn) (*GetCredentialsInWalletResponse, error) @@ -1476,6 +1473,9 @@ type ClientWithResponsesInterface interface { LoadVCWithResponse(ctx context.Context, subjectID string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*LoadVCResponse, error) + // RemoveCredentialFromWalletWithResponse request + RemoveCredentialFromWalletWithResponse(ctx context.Context, subjectID string, id string, reqEditors ...RequestEditorFn) (*RemoveCredentialFromWalletResponse, error) + // IssueVCWithBodyWithResponse request with any body IssueVCWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*IssueVCResponse, error) @@ -1554,9 +1554,10 @@ func (r CreateVPResponse) StatusCode() int { return 0 } -type RemoveCredentialFromWalletResponse struct { +type GetCredentialsInWalletResponse struct { Body []byte HTTPResponse *http.Response + JSON200 *[]VerifiableCredential ApplicationproblemJSONDefault *struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1570,7 +1571,7 @@ type RemoveCredentialFromWalletResponse struct { } // Status returns HTTPResponse.Status -func (r RemoveCredentialFromWalletResponse) Status() string { +func (r GetCredentialsInWalletResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -1578,17 +1579,16 @@ func (r RemoveCredentialFromWalletResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r RemoveCredentialFromWalletResponse) StatusCode() int { +func (r GetCredentialsInWalletResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type GetCredentialsInWalletResponse struct { +type LoadVCResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *[]VerifiableCredential ApplicationproblemJSONDefault *struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1602,7 +1602,7 @@ type GetCredentialsInWalletResponse struct { } // Status returns HTTPResponse.Status -func (r GetCredentialsInWalletResponse) Status() string { +func (r LoadVCResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -1610,14 +1610,14 @@ func (r GetCredentialsInWalletResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r GetCredentialsInWalletResponse) StatusCode() int { +func (r LoadVCResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type LoadVCResponse struct { +type RemoveCredentialFromWalletResponse struct { Body []byte HTTPResponse *http.Response ApplicationproblemJSONDefault *struct { @@ -1633,7 +1633,7 @@ type LoadVCResponse struct { } // Status returns HTTPResponse.Status -func (r LoadVCResponse) Status() string { +func (r RemoveCredentialFromWalletResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -1641,7 +1641,7 @@ func (r LoadVCResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r LoadVCResponse) StatusCode() int { +func (r RemoveCredentialFromWalletResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -2015,15 +2015,6 @@ func (c *ClientWithResponses) CreateVPWithResponse(ctx context.Context, body Cre return ParseCreateVPResponse(rsp) } -// RemoveCredentialFromWalletWithResponse request returning *RemoveCredentialFromWalletResponse -func (c *ClientWithResponses) RemoveCredentialFromWalletWithResponse(ctx context.Context, did string, id string, reqEditors ...RequestEditorFn) (*RemoveCredentialFromWalletResponse, error) { - rsp, err := c.RemoveCredentialFromWallet(ctx, did, id, reqEditors...) - if err != nil { - return nil, err - } - return ParseRemoveCredentialFromWalletResponse(rsp) -} - // GetCredentialsInWalletWithResponse request returning *GetCredentialsInWalletResponse func (c *ClientWithResponses) GetCredentialsInWalletWithResponse(ctx context.Context, subjectID string, reqEditors ...RequestEditorFn) (*GetCredentialsInWalletResponse, error) { rsp, err := c.GetCredentialsInWallet(ctx, subjectID, reqEditors...) @@ -2050,6 +2041,15 @@ func (c *ClientWithResponses) LoadVCWithResponse(ctx context.Context, subjectID return ParseLoadVCResponse(rsp) } +// RemoveCredentialFromWalletWithResponse request returning *RemoveCredentialFromWalletResponse +func (c *ClientWithResponses) RemoveCredentialFromWalletWithResponse(ctx context.Context, subjectID string, id string, reqEditors ...RequestEditorFn) (*RemoveCredentialFromWalletResponse, error) { + rsp, err := c.RemoveCredentialFromWallet(ctx, subjectID, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseRemoveCredentialFromWalletResponse(rsp) +} + // IssueVCWithBodyWithResponse request with arbitrary body returning *IssueVCResponse func (c *ClientWithResponses) IssueVCWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*IssueVCResponse, error) { rsp, err := c.IssueVCWithBody(ctx, contentType, body, reqEditors...) @@ -2239,20 +2239,27 @@ func ParseCreateVPResponse(rsp *http.Response) (*CreateVPResponse, error) { return response, nil } -// ParseRemoveCredentialFromWalletResponse parses an HTTP response from a RemoveCredentialFromWalletWithResponse call -func ParseRemoveCredentialFromWalletResponse(rsp *http.Response) (*RemoveCredentialFromWalletResponse, error) { +// ParseGetCredentialsInWalletResponse parses an HTTP response from a GetCredentialsInWalletWithResponse call +func ParseGetCredentialsInWalletResponse(rsp *http.Response) (*GetCredentialsInWalletResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &RemoveCredentialFromWalletResponse{ + response := &GetCredentialsInWalletResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []VerifiableCredential + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: var dest struct { // Detail A human-readable explanation specific to this occurrence of the problem. @@ -2274,27 +2281,20 @@ func ParseRemoveCredentialFromWalletResponse(rsp *http.Response) (*RemoveCredent return response, nil } -// ParseGetCredentialsInWalletResponse parses an HTTP response from a GetCredentialsInWalletWithResponse call -func ParseGetCredentialsInWalletResponse(rsp *http.Response) (*GetCredentialsInWalletResponse, error) { +// ParseLoadVCResponse parses an HTTP response from a LoadVCWithResponse call +func ParseLoadVCResponse(rsp *http.Response) (*LoadVCResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetCredentialsInWalletResponse{ + response := &LoadVCResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []VerifiableCredential - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: var dest struct { // Detail A human-readable explanation specific to this occurrence of the problem. @@ -2316,15 +2316,15 @@ func ParseGetCredentialsInWalletResponse(rsp *http.Response) (*GetCredentialsInW return response, nil } -// ParseLoadVCResponse parses an HTTP response from a LoadVCWithResponse call -func ParseLoadVCResponse(rsp *http.Response) (*LoadVCResponse, error) { +// ParseRemoveCredentialFromWalletResponse parses an HTTP response from a RemoveCredentialFromWalletWithResponse call +func ParseRemoveCredentialFromWalletResponse(rsp *http.Response) (*RemoveCredentialFromWalletResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &LoadVCResponse{ + response := &RemoveCredentialFromWalletResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -2804,15 +2804,15 @@ type ServerInterface interface { // Create a new Verifiable Presentation for a set of Verifiable Credentials. // (POST /internal/vcr/v2/holder/vp) CreateVP(ctx echo.Context) error - // Remove a VerifiableCredential from the holders wallet. - // (DELETE /internal/vcr/v2/holder/{did}/vc/{id}) - RemoveCredentialFromWallet(ctx echo.Context, did string, id string) error // List all Verifiable Credentials in the holder's wallet. // (GET /internal/vcr/v2/holder/{subjectID}/vc) GetCredentialsInWallet(ctx echo.Context, subjectID string) error // Load a VerifiableCredential into the holders wallet. // (POST /internal/vcr/v2/holder/{subjectID}/vc) LoadVC(ctx echo.Context, subjectID string) error + // Remove a VerifiableCredential from the holders wallet. + // (DELETE /internal/vcr/v2/holder/{subjectID}/vc/{id}) + RemoveCredentialFromWallet(ctx echo.Context, subjectID string, id string) error // Issues a new Verifiable Credential // (POST /internal/vcr/v2/issuer/vc) IssueVC(ctx echo.Context) error @@ -2864,31 +2864,23 @@ func (w *ServerInterfaceWrapper) CreateVP(ctx echo.Context) error { return err } -// RemoveCredentialFromWallet converts echo context to params. -func (w *ServerInterfaceWrapper) RemoveCredentialFromWallet(ctx echo.Context) error { +// GetCredentialsInWallet converts echo context to params. +func (w *ServerInterfaceWrapper) GetCredentialsInWallet(ctx echo.Context) error { var err error - // ------------- Path parameter "did" ------------- - var did string - - did = ctx.Param("did") - - // ------------- Path parameter "id" ------------- - var id string + // ------------- Path parameter "subjectID" ------------- + var subjectID string - err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) - } + subjectID = ctx.Param("subjectID") ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.RemoveCredentialFromWallet(ctx, did, id) + err = w.Handler.GetCredentialsInWallet(ctx, subjectID) return err } -// GetCredentialsInWallet converts echo context to params. -func (w *ServerInterfaceWrapper) GetCredentialsInWallet(ctx echo.Context) error { +// LoadVC converts echo context to params. +func (w *ServerInterfaceWrapper) LoadVC(ctx echo.Context) error { var err error // ------------- Path parameter "subjectID" ------------- var subjectID string @@ -2898,22 +2890,30 @@ func (w *ServerInterfaceWrapper) GetCredentialsInWallet(ctx echo.Context) error ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.GetCredentialsInWallet(ctx, subjectID) + err = w.Handler.LoadVC(ctx, subjectID) return err } -// LoadVC converts echo context to params. -func (w *ServerInterfaceWrapper) LoadVC(ctx echo.Context) error { +// RemoveCredentialFromWallet converts echo context to params. +func (w *ServerInterfaceWrapper) RemoveCredentialFromWallet(ctx echo.Context) error { var err error // ------------- Path parameter "subjectID" ------------- var subjectID string subjectID = ctx.Param("subjectID") + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.LoadVC(ctx, subjectID) + err = w.Handler.RemoveCredentialFromWallet(ctx, subjectID, id) return err } @@ -3118,9 +3118,9 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL } router.POST(baseURL+"/internal/vcr/v2/holder/vp", wrapper.CreateVP) - router.DELETE(baseURL+"/internal/vcr/v2/holder/:did/vc/:id", wrapper.RemoveCredentialFromWallet) router.GET(baseURL+"/internal/vcr/v2/holder/:subjectID/vc", wrapper.GetCredentialsInWallet) router.POST(baseURL+"/internal/vcr/v2/holder/:subjectID/vc", wrapper.LoadVC) + router.DELETE(baseURL+"/internal/vcr/v2/holder/:subjectID/vc/:id", wrapper.RemoveCredentialFromWallet) router.POST(baseURL+"/internal/vcr/v2/issuer/vc", wrapper.IssueVC) router.GET(baseURL+"/internal/vcr/v2/issuer/vc/search", wrapper.SearchIssuedVCs) router.DELETE(baseURL+"/internal/vcr/v2/issuer/vc/:id", wrapper.RevokeVC) @@ -3173,24 +3173,24 @@ func (response CreateVPdefaultApplicationProblemPlusJSONResponse) VisitCreateVPR return json.NewEncoder(w).Encode(response.Body) } -type RemoveCredentialFromWalletRequestObject struct { - Did string `json:"did"` - Id string `json:"id"` +type GetCredentialsInWalletRequestObject struct { + SubjectID string `json:"subjectID"` } -type RemoveCredentialFromWalletResponseObject interface { - VisitRemoveCredentialFromWalletResponse(w http.ResponseWriter) error +type GetCredentialsInWalletResponseObject interface { + VisitGetCredentialsInWalletResponse(w http.ResponseWriter) error } -type RemoveCredentialFromWallet204Response struct { -} +type GetCredentialsInWallet200JSONResponse []VerifiableCredential -func (response RemoveCredentialFromWallet204Response) VisitRemoveCredentialFromWalletResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil +func (response GetCredentialsInWallet200JSONResponse) VisitGetCredentialsInWalletResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) } -type RemoveCredentialFromWalletdefaultApplicationProblemPlusJSONResponse struct { +type GetCredentialsInWalletdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -3204,31 +3204,31 @@ type RemoveCredentialFromWalletdefaultApplicationProblemPlusJSONResponse struct StatusCode int } -func (response RemoveCredentialFromWalletdefaultApplicationProblemPlusJSONResponse) VisitRemoveCredentialFromWalletResponse(w http.ResponseWriter) error { +func (response GetCredentialsInWalletdefaultApplicationProblemPlusJSONResponse) VisitGetCredentialsInWalletResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) return json.NewEncoder(w).Encode(response.Body) } -type GetCredentialsInWalletRequestObject struct { +type LoadVCRequestObject struct { SubjectID string `json:"subjectID"` + Body *LoadVCJSONRequestBody } -type GetCredentialsInWalletResponseObject interface { - VisitGetCredentialsInWalletResponse(w http.ResponseWriter) error +type LoadVCResponseObject interface { + VisitLoadVCResponse(w http.ResponseWriter) error } -type GetCredentialsInWallet200JSONResponse []VerifiableCredential - -func (response GetCredentialsInWallet200JSONResponse) VisitGetCredentialsInWalletResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) +type LoadVC204Response struct { +} - return json.NewEncoder(w).Encode(response) +func (response LoadVC204Response) VisitLoadVCResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil } -type GetCredentialsInWalletdefaultApplicationProblemPlusJSONResponse struct { +type LoadVCdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -3242,31 +3242,31 @@ type GetCredentialsInWalletdefaultApplicationProblemPlusJSONResponse struct { StatusCode int } -func (response GetCredentialsInWalletdefaultApplicationProblemPlusJSONResponse) VisitGetCredentialsInWalletResponse(w http.ResponseWriter) error { +func (response LoadVCdefaultApplicationProblemPlusJSONResponse) VisitLoadVCResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) return json.NewEncoder(w).Encode(response.Body) } -type LoadVCRequestObject struct { +type RemoveCredentialFromWalletRequestObject struct { SubjectID string `json:"subjectID"` - Body *LoadVCJSONRequestBody + Id string `json:"id"` } -type LoadVCResponseObject interface { - VisitLoadVCResponse(w http.ResponseWriter) error +type RemoveCredentialFromWalletResponseObject interface { + VisitRemoveCredentialFromWalletResponse(w http.ResponseWriter) error } -type LoadVC204Response struct { +type RemoveCredentialFromWallet204Response struct { } -func (response LoadVC204Response) VisitLoadVCResponse(w http.ResponseWriter) error { +func (response RemoveCredentialFromWallet204Response) VisitRemoveCredentialFromWalletResponse(w http.ResponseWriter) error { w.WriteHeader(204) return nil } -type LoadVCdefaultApplicationProblemPlusJSONResponse struct { +type RemoveCredentialFromWalletdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -3280,7 +3280,7 @@ type LoadVCdefaultApplicationProblemPlusJSONResponse struct { StatusCode int } -func (response LoadVCdefaultApplicationProblemPlusJSONResponse) VisitLoadVCResponse(w http.ResponseWriter) error { +func (response RemoveCredentialFromWalletdefaultApplicationProblemPlusJSONResponse) VisitRemoveCredentialFromWalletResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) @@ -3716,15 +3716,15 @@ type StrictServerInterface interface { // Create a new Verifiable Presentation for a set of Verifiable Credentials. // (POST /internal/vcr/v2/holder/vp) CreateVP(ctx context.Context, request CreateVPRequestObject) (CreateVPResponseObject, error) - // Remove a VerifiableCredential from the holders wallet. - // (DELETE /internal/vcr/v2/holder/{did}/vc/{id}) - RemoveCredentialFromWallet(ctx context.Context, request RemoveCredentialFromWalletRequestObject) (RemoveCredentialFromWalletResponseObject, error) // List all Verifiable Credentials in the holder's wallet. // (GET /internal/vcr/v2/holder/{subjectID}/vc) GetCredentialsInWallet(ctx context.Context, request GetCredentialsInWalletRequestObject) (GetCredentialsInWalletResponseObject, error) // Load a VerifiableCredential into the holders wallet. // (POST /internal/vcr/v2/holder/{subjectID}/vc) LoadVC(ctx context.Context, request LoadVCRequestObject) (LoadVCResponseObject, error) + // Remove a VerifiableCredential from the holders wallet. + // (DELETE /internal/vcr/v2/holder/{subjectID}/vc/{id}) + RemoveCredentialFromWallet(ctx context.Context, request RemoveCredentialFromWalletRequestObject) (RemoveCredentialFromWalletResponseObject, error) // Issues a new Verifiable Credential // (POST /internal/vcr/v2/issuer/vc) IssueVC(ctx context.Context, request IssueVCRequestObject) (IssueVCResponseObject, error) @@ -3801,32 +3801,6 @@ func (sh *strictHandler) CreateVP(ctx echo.Context) error { return nil } -// RemoveCredentialFromWallet operation middleware -func (sh *strictHandler) RemoveCredentialFromWallet(ctx echo.Context, did string, id string) error { - var request RemoveCredentialFromWalletRequestObject - - request.Did = did - request.Id = id - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.RemoveCredentialFromWallet(ctx.Request().Context(), request.(RemoveCredentialFromWalletRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "RemoveCredentialFromWallet") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(RemoveCredentialFromWalletResponseObject); ok { - return validResponse.VisitRemoveCredentialFromWalletResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - // GetCredentialsInWallet operation middleware func (sh *strictHandler) GetCredentialsInWallet(ctx echo.Context, subjectID string) error { var request GetCredentialsInWalletRequestObject @@ -3883,6 +3857,32 @@ func (sh *strictHandler) LoadVC(ctx echo.Context, subjectID string) error { return nil } +// RemoveCredentialFromWallet operation middleware +func (sh *strictHandler) RemoveCredentialFromWallet(ctx echo.Context, subjectID string, id string) error { + var request RemoveCredentialFromWalletRequestObject + + request.SubjectID = subjectID + request.Id = id + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.RemoveCredentialFromWallet(ctx.Request().Context(), request.(RemoveCredentialFromWalletRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RemoveCredentialFromWallet") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(RemoveCredentialFromWalletResponseObject); ok { + return validResponse.VisitRemoveCredentialFromWalletResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // IssueVC operation middleware func (sh *strictHandler) IssueVC(ctx echo.Context) error { var request IssueVCRequestObject From 3c758a1197feae112e8bf2afc115728a966873ef Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Mon, 14 Oct 2024 09:37:06 +0200 Subject: [PATCH 33/72] pass problem details when discovery service returns them (#3476) --- discovery/api/server/client/http.go | 20 ++++++++++++++++++-- discovery/api/server/client/http_test.go | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/discovery/api/server/client/http.go b/discovery/api/server/client/http.go index a9f33877b..84695dde1 100644 --- a/discovery/api/server/client/http.go +++ b/discovery/api/server/client/http.go @@ -65,7 +65,8 @@ func (h DefaultHTTPClient) Register(ctx context.Context, serviceEndpointURL stri } defer httpResponse.Body.Close() if err := core.TestResponseCodeWithLog(201, httpResponse, log.Logger()); err != nil { - return fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err) + httpErr := err.(core.HttpError) // TestResponseCodeWithLog always returns an HttpError + return fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %s", serviceEndpointURL, problemResponseToError(httpErr)) } return nil } @@ -83,7 +84,8 @@ func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, t } defer httpResponse.Body.Close() if err := core.TestResponseCode(200, httpResponse); err != nil { - return nil, 0, fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err) + httpErr := err.(core.HttpError) // TestResponseCodeWithLog always returns an HttpError + return nil, 0, fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %s", serviceEndpointURL, problemResponseToError(httpErr)) } responseData, err := io.ReadAll(httpResponse.Body) if err != nil { @@ -95,3 +97,17 @@ func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, t } return result.Entries, result.Timestamp, nil } + +// problemResponseToError converts a Problem Details response to an error. +// It creates an error with the given string concatenated with the title and detail fields of the problem details. +func problemResponseToError(httpErr core.HttpError) string { + var problemDetails struct { + Title string `json:"title"` + Description string `json:"detail"` + Status int `json:"status"` + } + if err := json.Unmarshal(httpErr.ResponseBody, &problemDetails); err != nil { + return fmt.Sprintf("%s: %s", httpErr.Error(), httpErr.ResponseBody) + } + return fmt.Sprintf("server returned HTTP status code %d: %s: %s", problemDetails.Status, problemDetails.Title, problemDetails.Description) +} diff --git a/discovery/api/server/client/http_test.go b/discovery/api/server/client/http_test.go index 9387dc5fd..c83262180 100644 --- a/discovery/api/server/client/http_test.go +++ b/discovery/api/server/client/http_test.go @@ -49,13 +49,25 @@ func TestHTTPInvoker_Register(t *testing.T) { assert.Equal(t, "application/json", handler.Request.Header.Get("Content-Type")) assert.Equal(t, vpData, handler.RequestData) }) - t.Run("non-ok", func(t *testing.T) { - server := httptest.NewServer(&testHTTP.Handler{StatusCode: http.StatusInternalServerError}) + t.Run("non-ok with problem details", func(t *testing.T) { + server := httptest.NewServer(&testHTTP.Handler{StatusCode: http.StatusBadRequest, ResponseData: `{"title":"missing credentials", "status":400, "detail":"could not resolve DID"}`}) client := New(false, time.Minute, server.TLS) err := client.Register(context.Background(), server.URL, vp) assert.ErrorContains(t, err, "non-OK response from remote Discovery Service") + assert.ErrorContains(t, err, "server returned HTTP status code 400") + assert.ErrorContains(t, err, "missing credentials: could not resolve DID") + }) + t.Run("non-ok other", func(t *testing.T) { + server := httptest.NewServer(&testHTTP.Handler{StatusCode: http.StatusNotFound, ResponseData: `not found`}) + client := New(false, time.Minute, server.TLS) + + err := client.Register(context.Background(), server.URL, vp) + + assert.ErrorContains(t, err, "non-OK response from remote Discovery Service") + assert.ErrorContains(t, err, "server returned HTTP 404") + assert.ErrorContains(t, err, "not found") }) } @@ -113,13 +125,15 @@ func TestHTTPInvoker_Get(t *testing.T) { assert.True(t, strings.HasPrefix(capturedRequest.Header.Get("X-Forwarded-Host"), "127.0.0.1")) }) t.Run("server returns invalid status code", func(t *testing.T) { - handler := &testHTTP.Handler{StatusCode: http.StatusInternalServerError} + handler := &testHTTP.Handler{StatusCode: http.StatusInternalServerError, ResponseData: `{"title":"internal server error", "status":500, "detail":"db not found"}`} server := httptest.NewServer(handler) client := New(false, time.Minute, server.TLS) _, _, err := client.Get(context.Background(), server.URL, 0) assert.ErrorContains(t, err, "non-OK response from remote Discovery Service") + assert.ErrorContains(t, err, "server returned HTTP status code 500") + assert.ErrorContains(t, err, "internal server error: db not found") }) t.Run("server does not return JSON", func(t *testing.T) { handler := &testHTTP.Handler{StatusCode: http.StatusOK} From 5d334e5c2f658509d96e3816026622bfbe4a368a Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:38:09 +0200 Subject: [PATCH 34/72] Check RW permissions in datadir (#3478) * Check RW permissions in datadir * fix file permission bits --- storage/engine.go | 36 ++++++++++++++++++++++++++++++++++++ storage/engine_test.go | 3 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/storage/engine.go b/storage/engine.go index 9b70099b2..c1834217e 100644 --- a/storage/engine.go +++ b/storage/engine.go @@ -24,6 +24,7 @@ import ( "fmt" "os" "path" + "path/filepath" "strings" "sync" "time" @@ -147,6 +148,12 @@ func (e *engine) Shutdown() error { func (e *engine) Configure(config core.ServerConfig) error { e.datadir = config.Datadir + err := confirmWriteAccess(e.datadir) + if err != nil { + return err + } + + // KV-storage if e.config.Redis.isConfigured() { redisDB, err := createRedisDatabase(e.config.Redis) if err != nil { @@ -163,10 +170,12 @@ func (e *engine) Configure(config core.ServerConfig) error { } e.databases = append(e.databases, bboltDB) + // SQL storage if err := e.initSQLDatabase(); err != nil { return fmt.Errorf("failed to initialize SQL database: %w", err) } + // session storage redisConfig := e.config.Session.Redis if redisConfig.isConfigured() { redisDB, err := createRedisDatabase(redisConfig) @@ -367,6 +376,33 @@ func (p *provider) getStore(moduleName string, name string, adapter database) (s return store, err } +func confirmWriteAccess(datadir string) error { + // Make sure the data directory exists + err := os.MkdirAll(path.Dir(datadir+string(os.PathSeparator)), os.ModePerm) + if err != nil { + // log error: "unable to create datadir (dir=./data): mkdir ./data: read-only file system" + return err + } + filename := filepath.Join(datadir, "rw-access-test-file") + // open/create file with read-write permission + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + // log error: "unable to configure Storage: open data/rw-access-test-file: read-only file system" + return err + } + // cleanup + err = file.Close() + if err != nil { + return err + } + // removing the file could cause issues if it was a pre-existing user file + err = os.Remove(filename) + if err != nil { + return err + } + return nil +} + type logrusInfoLogWriter struct { } diff --git a/storage/engine_test.go b/storage/engine_test.go index 121a21367..f31593a76 100644 --- a/storage/engine_test.go +++ b/storage/engine_test.go @@ -119,7 +119,8 @@ func Test_engine_sqlDatabase(t *testing.T) { dataDir := io.TestDirectory(t) require.NoError(t, os.Remove(dataDir)) e := New() - err := e.Configure(core.ServerConfig{Datadir: dataDir}) + e.(*engine).datadir = dataDir + err := e.(*engine).initSQLDatabase() assert.ErrorContains(t, err, "unable to open database file") }) t.Run("sqlite is restricted to 1 connection", func(t *testing.T) { From 60a9a3fa5c5fd9752095ba0f8ffc1ff7ebd35a75 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Mon, 14 Oct 2024 10:27:20 +0200 Subject: [PATCH 35/72] Add seed to discovery service protocol (#3479) --- discovery/api/server/api.go | 3 +- discovery/api/server/api_test.go | 8 +- discovery/api/server/client/http.go | 14 +-- discovery/api/server/client/http_test.go | 13 ++- discovery/api/server/client/interface.go | 2 +- discovery/api/server/client/mock.go | 9 +- discovery/api/server/client/types.go | 2 + discovery/client.go | 9 +- discovery/client_test.go | 47 +++++--- discovery/interface.go | 2 +- discovery/mock.go | 9 +- discovery/module.go | 8 +- discovery/module_test.go | 101 ++++++++++------- discovery/store.go | 59 ++++++++-- discovery/store_test.go | 103 +++++++++++++----- discovery/test.go | 2 + docs/_static/discovery/server.yaml | 4 + .../009_discoveryservice_seed.sql | 6 + 18 files changed, 282 insertions(+), 119 deletions(-) create mode 100644 storage/sql_migrations/009_discoveryservice_seed.sql diff --git a/discovery/api/server/api.go b/discovery/api/server/api.go index dd519d443..fe8b3d2b7 100644 --- a/discovery/api/server/api.go +++ b/discovery/api/server/api.go @@ -71,11 +71,12 @@ func (w *Wrapper) GetPresentations(ctx context.Context, request GetPresentations timestamp = *request.Params.Timestamp } - presentations, newTimestamp, err := w.Server.Get(contextWithForwardedHost(ctx), request.ServiceID, timestamp) + presentations, seed, newTimestamp, err := w.Server.Get(contextWithForwardedHost(ctx), request.ServiceID, timestamp) if err != nil { return nil, err } return GetPresentations200JSONResponse{ + Seed: seed, Entries: presentations, Timestamp: newTimestamp, }, nil diff --git a/discovery/api/server/api_test.go b/discovery/api/server/api_test.go index 9092aef46..e04bd02f2 100644 --- a/discovery/api/server/api_test.go +++ b/discovery/api/server/api_test.go @@ -35,10 +35,11 @@ const serviceID = "wonderland" func TestWrapper_GetPresentations(t *testing.T) { lastTimestamp := 1 presentations := map[string]vc.VerifiablePresentation{} + seed := "seed" ctx := context.Background() t.Run("no timestamp", func(t *testing.T) { test := newMockContext(t) - test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(presentations, lastTimestamp, nil) + test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(presentations, seed, lastTimestamp, nil) response, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{ServiceID: serviceID}) @@ -46,11 +47,12 @@ func TestWrapper_GetPresentations(t *testing.T) { require.IsType(t, GetPresentations200JSONResponse{}, response) assert.Equal(t, lastTimestamp, response.(GetPresentations200JSONResponse).Timestamp) assert.Equal(t, presentations, response.(GetPresentations200JSONResponse).Entries) + assert.Equal(t, seed, response.(GetPresentations200JSONResponse).Seed) }) t.Run("with timestamp", func(t *testing.T) { givenTimestamp := 1 test := newMockContext(t) - test.server.EXPECT().Get(gomock.Any(), serviceID, 1).Return(presentations, lastTimestamp, nil) + test.server.EXPECT().Get(gomock.Any(), serviceID, 1).Return(presentations, seed, lastTimestamp, nil) response, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{ ServiceID: serviceID, @@ -66,7 +68,7 @@ func TestWrapper_GetPresentations(t *testing.T) { }) t.Run("error", func(t *testing.T) { test := newMockContext(t) - test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(nil, 0, errors.New("foo")) + test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(nil, "", 0, errors.New("foo")) _, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{ServiceID: serviceID}) diff --git a/discovery/api/server/client/http.go b/discovery/api/server/client/http.go index 84695dde1..6623b642a 100644 --- a/discovery/api/server/client/http.go +++ b/discovery/api/server/client/http.go @@ -71,31 +71,31 @@ func (h DefaultHTTPClient) Register(ctx context.Context, serviceEndpointURL stri return nil } -func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, int, error) { +func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, string, int, error) { httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, serviceEndpointURL, nil) httpRequest.URL.RawQuery = url.Values{"timestamp": []string{fmt.Sprintf("%d", timestamp)}}.Encode() if err != nil { - return nil, 0, err + return nil, "", 0, err } httpRequest.Header.Set("X-Forwarded-Host", httpRequest.Host) // prevent cycles httpResponse, err := h.client.Do(httpRequest) if err != nil { - return nil, 0, fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err) + return nil, "", 0, fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err) } defer httpResponse.Body.Close() if err := core.TestResponseCode(200, httpResponse); err != nil { httpErr := err.(core.HttpError) // TestResponseCodeWithLog always returns an HttpError - return nil, 0, fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %s", serviceEndpointURL, problemResponseToError(httpErr)) + return nil, "", 0, fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %s", serviceEndpointURL, problemResponseToError(httpErr)) } responseData, err := io.ReadAll(httpResponse.Body) if err != nil { - return nil, 0, fmt.Errorf("failed to read response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err) + return nil, "", 0, fmt.Errorf("failed to read response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err) } var result PresentationsResponse if err := json.Unmarshal(responseData, &result); err != nil { - return nil, 0, fmt.Errorf("failed to unmarshal response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err) + return nil, "", 0, fmt.Errorf("failed to unmarshal response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err) } - return result.Entries, result.Timestamp, nil + return result.Entries, result.Seed, result.Timestamp, nil } // problemResponseToError converts a Problem Details response to an error. diff --git a/discovery/api/server/client/http_test.go b/discovery/api/server/client/http_test.go index c83262180..bfe0717ce 100644 --- a/discovery/api/server/client/http_test.go +++ b/discovery/api/server/client/http_test.go @@ -79,29 +79,32 @@ func TestHTTPInvoker_Get(t *testing.T) { t.Run("no timestamp from client", func(t *testing.T) { handler := &testHTTP.Handler{StatusCode: http.StatusOK} handler.ResponseData = map[string]interface{}{ + "seed": "seed", "entries": map[string]interface{}{"1": vp}, "timestamp": 1, } server := httptest.NewServer(handler) client := New(false, time.Minute, server.TLS) - presentations, timestamp, err := client.Get(context.Background(), server.URL, 0) + presentations, seed, timestamp, err := client.Get(context.Background(), server.URL, 0) assert.NoError(t, err) assert.Len(t, presentations, 1) assert.Equal(t, "0", handler.RequestQuery.Get("timestamp")) assert.Equal(t, 1, timestamp) + assert.Equal(t, "seed", seed) }) t.Run("timestamp provided by client", func(t *testing.T) { handler := &testHTTP.Handler{StatusCode: http.StatusOK} handler.ResponseData = map[string]interface{}{ + "seed": "seed", "entries": map[string]interface{}{"1": vp}, "timestamp": 1, } server := httptest.NewServer(handler) client := New(false, time.Minute, server.TLS) - presentations, timestamp, err := client.Get(context.Background(), server.URL, 1) + presentations, _, timestamp, err := client.Get(context.Background(), server.URL, 1) assert.NoError(t, err) assert.Len(t, presentations, 1) @@ -119,7 +122,7 @@ func TestHTTPInvoker_Get(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handler)) client := New(false, time.Minute, server.TLS) - _, _, err := client.Get(context.Background(), server.URL, 0) + _, _, _, err := client.Get(context.Background(), server.URL, 0) require.NoError(t, err) assert.True(t, strings.HasPrefix(capturedRequest.Header.Get("X-Forwarded-Host"), "127.0.0.1")) @@ -129,7 +132,7 @@ func TestHTTPInvoker_Get(t *testing.T) { server := httptest.NewServer(handler) client := New(false, time.Minute, server.TLS) - _, _, err := client.Get(context.Background(), server.URL, 0) + _, _, _, err := client.Get(context.Background(), server.URL, 0) assert.ErrorContains(t, err, "non-OK response from remote Discovery Service") assert.ErrorContains(t, err, "server returned HTTP status code 500") @@ -141,7 +144,7 @@ func TestHTTPInvoker_Get(t *testing.T) { server := httptest.NewServer(handler) client := New(false, time.Minute, server.TLS) - _, _, err := client.Get(context.Background(), server.URL, 0) + _, _, _, err := client.Get(context.Background(), server.URL, 0) assert.ErrorContains(t, err, "failed to unmarshal response from remote Discovery Service") }) diff --git a/discovery/api/server/client/interface.go b/discovery/api/server/client/interface.go index 24087f718..10722ea8c 100644 --- a/discovery/api/server/client/interface.go +++ b/discovery/api/server/client/interface.go @@ -31,5 +31,5 @@ type HTTPClient interface { // Get retrieves Verifiable Presentations from the remote Discovery Service, that were added since the given timestamp. // If the call succeeds it returns the Verifiable Presentations and the timestamp that was returned by the server. // If the given timestamp is 0, all Verifiable Presentations are retrieved. - Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, int, error) + Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, string, int, error) } diff --git a/discovery/api/server/client/mock.go b/discovery/api/server/client/mock.go index 2fe595a28..895718a0d 100644 --- a/discovery/api/server/client/mock.go +++ b/discovery/api/server/client/mock.go @@ -41,13 +41,14 @@ func (m *MockHTTPClient) EXPECT() *MockHTTPClientMockRecorder { } // Get mocks base method. -func (m *MockHTTPClient) Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, int, error) { +func (m *MockHTTPClient) Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, string, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ctx, serviceEndpointURL, timestamp) ret0, _ := ret[0].(map[string]vc.VerifiablePresentation) - ret1, _ := ret[1].(int) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(int) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 } // Get indicates an expected call of Get. diff --git a/discovery/api/server/client/types.go b/discovery/api/server/client/types.go index 89c17609e..184a4a0a8 100644 --- a/discovery/api/server/client/types.go +++ b/discovery/api/server/client/types.go @@ -24,6 +24,8 @@ import "github.com/nuts-foundation/go-did/vc" type PresentationsResponse struct { // Entries contains mappings from timestamp (as string) to a VerifiablePresentation. Entries map[string]vc.VerifiablePresentation `json:"entries"` + // Seed is a unique value for the combination of serviceID and a server instance. + Seed string `json:"seed"` // Timestamp is the timestamp of the latest entry. It's not a unix timestamp but a Lamport Clock. Timestamp int `json:"timestamp"` } diff --git a/discovery/client.go b/discovery/client.go index 11a4665ce..e22d4b351 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -367,16 +367,21 @@ func (u *clientUpdater) updateService(ctx context.Context, service ServiceDefini log.Logger(). WithField("discoveryService", service.ID). Tracef("Checking for new Verifiable Presentations from Discovery Service (timestamp: %d)", currentTimestamp) - presentations, serverTimestamp, err := u.client.Get(ctx, service.Endpoint, currentTimestamp) + presentations, seed, serverTimestamp, err := u.client.Get(ctx, service.Endpoint, currentTimestamp) if err != nil { return fmt.Errorf("failed to get presentations from discovery service (id=%s): %w", service.ID, err) } + // check testSeed in store, wipe if it's different. Done by the store for transaction safety. + err = u.store.wipeOnSeedChange(service.ID, seed) + if err != nil { + return fmt.Errorf("failed to wipe on testSeed change (service=%s, testSeed=%s): %w", service.ID, seed, err) + } for _, presentation := range presentations { if err := u.verifier(service, presentation); err != nil { log.Logger().WithError(err).Warnf("Presentation verification failed, not adding it (service=%s, id=%s)", service.ID, presentation.ID) continue } - if err := u.store.add(service.ID, presentation, serverTimestamp); err != nil { + if err := u.store.add(service.ID, presentation, seed, serverTimestamp); err != nil { return fmt.Errorf("failed to store presentation (service=%s, id=%s): %w", service.ID, presentation.ID, err) } log.Logger(). diff --git a/discovery/client_test.go b/discovery/client_test.go index 4098df696..0ab14a8c4 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -221,7 +221,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()) ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1)) err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) @@ -236,7 +236,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { claims["retract_jti"] = vpAlice.ID.String() vp.Type = append(vp.Type, retractionPresentationType) }, vcAlice) - require.NoError(t, ctx.store.add(testServiceID, vpAliceDeactivated, 1)) + require.NoError(t, ctx.store.add(testServiceID, vpAliceDeactivated, testSeed, 1)) err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) @@ -255,7 +255,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")) ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1)) err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) @@ -266,7 +266,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { ctx := newTestContext(t) ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil, assert.AnError) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1)) err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) @@ -394,7 +394,7 @@ func Test_clientUpdater_updateService(t *testing.T) { httpClient := client.NewMockHTTPClient(ctrl) updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) - httpClient.EXPECT().Get(ctx, testDefinitions()[testServiceID].Endpoint, 0).Return(map[string]vc.VerifiablePresentation{}, 0, nil) + httpClient.EXPECT().Get(ctx, testDefinitions()[testServiceID].Endpoint, 0).Return(map[string]vc.VerifiablePresentation{}, testSeed, 0, nil) err := updater.updateService(ctx, testDefinitions()[testServiceID]) @@ -406,7 +406,7 @@ func Test_clientUpdater_updateService(t *testing.T) { httpClient := client.NewMockHTTPClient(ctrl) updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) - httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, 1, nil) + httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, testSeed, 1, nil) err := updater.updateService(ctx, testDefinitions()[testServiceID]) @@ -423,7 +423,7 @@ func Test_clientUpdater_updateService(t *testing.T) { return nil }, httpClient) - httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice, "2": vpBob}, 2, nil) + httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice, "2": vpBob}, testSeed, 2, nil) err := updater.updateService(ctx, testDefinitions()[testServiceID]) @@ -440,28 +440,49 @@ func Test_clientUpdater_updateService(t *testing.T) { resetStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) httpClient := client.NewMockHTTPClient(ctrl) - err := store.setTimestamp(store.db, testServiceID, 1) + err := store.setTimestamp(store.db, testServiceID, testSeed, 1) require.NoError(t, err) updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) - httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 1).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, 1, nil) + httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 1).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, testSeed, 1, nil) err = updater.updateService(ctx, testDefinitions()[testServiceID]) require.NoError(t, err) }) + t.Run("seed change wipes entries", func(t *testing.T) { + resetStore(t, storageEngine.GetSQLDatabase()) + ctrl := gomock.NewController(t) + httpClient := client.NewMockHTTPClient(ctrl) + updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) + store.add(testServiceID, vpAlice, testSeed, 0) + + exists, err := store.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) + require.NoError(t, err) + require.True(t, exists) + + httpClient.EXPECT().Get(ctx, testDefinitions()[testServiceID].Endpoint, 1).Return(map[string]vc.VerifiablePresentation{}, "other", 0, nil) + + err = updater.updateService(ctx, testDefinitions()[testServiceID]) + + require.NoError(t, err) + exists, err = store.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) + require.NoError(t, err) + require.False(t, exists) + }) } func Test_clientUpdater_update(t *testing.T) { + seed := "seed" t.Run("proceeds when service update fails", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) store := setupStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) httpClient := client.NewMockHTTPClient(ctrl) - httpClient.EXPECT().Get(gomock.Any(), "http://example.com/usecase", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, 0, nil) - httpClient.EXPECT().Get(gomock.Any(), "http://example.com/other", gomock.Any()).Return(nil, 0, errors.New("test")) - httpClient.EXPECT().Get(gomock.Any(), "http://example.com/unsupported", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, 0, nil) + httpClient.EXPECT().Get(gomock.Any(), "http://example.com/usecase", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, seed, 0, nil) + httpClient.EXPECT().Get(gomock.Any(), "http://example.com/other", gomock.Any()).Return(nil, "", 0, errors.New("test")) + httpClient.EXPECT().Get(gomock.Any(), "http://example.com/unsupported", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, seed, 0, nil) updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) err := updater.update(context.Background()) @@ -474,7 +495,7 @@ func Test_clientUpdater_update(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) httpClient := client.NewMockHTTPClient(ctrl) - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, 0, nil).MinTimes(2) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, seed, 0, nil).MinTimes(2) updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) err := updater.update(context.Background()) diff --git a/discovery/interface.go b/discovery/interface.go index f2522d261..7a9cf5e92 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -48,7 +48,7 @@ type Server interface { Register(context context.Context, serviceID string, presentation vc.VerifiablePresentation) error // Get retrieves the presentations for the given service, starting from the given timestamp. // If the node is not configured as server for the given serviceID, the call will be forwarded to the configured server. - Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, int, error) + Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, string, int, error) } // Client defines the API for Discovery Clients. diff --git a/discovery/mock.go b/discovery/mock.go index 5466dd198..3d231317e 100644 --- a/discovery/mock.go +++ b/discovery/mock.go @@ -41,13 +41,14 @@ func (m *MockServer) EXPECT() *MockServerMockRecorder { } // Get mocks base method. -func (m *MockServer) Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, int, error) { +func (m *MockServer) Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, string, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", context, serviceID, startAfter) ret0, _ := ret[0].(map[string]vc.VerifiablePresentation) - ret1, _ := ret[1].(int) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(int) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 } // Get indicates an expected call of Get. diff --git a/discovery/module.go b/discovery/module.go index 0ad5f38e5..a34f45fed 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -203,7 +203,7 @@ func (m *Module) Register(context context.Context, serviceID string, presentatio return err } - return m.store.add(serviceID, presentation, 0) + return m.store.add(serviceID, presentation, "", 0) } func (m *Module) verifyRegistration(definition ServiceDefinition, presentation vc.VerifiablePresentation) error { @@ -327,18 +327,18 @@ func (m *Module) validateRetraction(serviceID string, presentation vc.Verifiable // Get is a Discovery Server function that retrieves the presentations for the given service, starting at timestamp+1. // See interface.go for more information. -func (m *Module) Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, int, error) { +func (m *Module) Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, string, int, error) { _, exists := m.serverDefinitions[serviceID] if !exists { // forward to configured server service, exists := m.allDefinitions[serviceID] if !exists { - return nil, 0, ErrServiceNotFound + return nil, "", 0, ErrServiceNotFound } // check If X-Forwarded-Host header is set, if set it must not be the same as service.Endpoint if cycleDetected(context, service) { - return nil, 0, errCyclicForwardingDetected + return nil, "", 0, errCyclicForwardingDetected } log.Logger().Infof("Forwarding Get request to configured server (service=%s)", serviceID) diff --git a/discovery/module_test.go b/discovery/module_test.go index 8e7d317bc..23f5f436e 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -39,6 +39,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "gorm.io/gorm" + "os" "testing" "time" ) @@ -65,9 +67,16 @@ func Test_Module_Register(t *testing.T) { err := m.Register(ctx, testServiceID, vpAlice) require.NoError(t, err) - _, timestamp, err := m.Get(ctx, testServiceID, 0) + _, seed, timestamp, err := m.Get(ctx, testServiceID, 0) require.NoError(t, err) assert.Equal(t, 1, timestamp) + assert.NotEmpty(t, seed) + + t.Run("already exists", func(t *testing.T) { + err = m.Register(ctx, testServiceID, vpAlice) + + assert.ErrorIs(t, err, ErrPresentationAlreadyExists) + }) }) t.Run("not a server", func(t *testing.T) { m, _ := setupModule(t, storageEngine, func(module *Module) { @@ -76,7 +85,7 @@ func Test_Module_Register(t *testing.T) { Endpoint: "https://example.com/someother", } mockhttpclient := module.httpClient.(*client.MockHTTPClient) - mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", gomock.Any()).Return(nil, 0, nil).AnyTimes() + mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", gomock.Any()).Return(nil, testSeed, 0, nil).AnyTimes() mockhttpclient.EXPECT().Register(gomock.Any(), "https://example.com/someother", vpAlice).Return(nil) }) @@ -91,19 +100,10 @@ func Test_Module_Register(t *testing.T) { err := m.Register(ctx, testServiceID, vpAlice) require.EqualError(t, err, "presentation is invalid for registration\npresentation verification failed: failed") - _, timestamp, err := m.Get(ctx, testServiceID, 0) + _, _, timestamp, err := m.Get(ctx, testServiceID, 0) require.NoError(t, err) assert.Equal(t, 0, timestamp) }) - t.Run("already exists", func(t *testing.T) { - m, testContext := setupModule(t, storageEngine) - testContext.verifier.EXPECT().VerifyVP(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - - err := m.Register(ctx, testServiceID, vpAlice) - assert.NoError(t, err) - err = m.Register(ctx, testServiceID, vpAlice) - assert.ErrorIs(t, err, ErrPresentationAlreadyExists) - }) t.Run("valid for too long", func(t *testing.T) { m, _ := setupModule(t, storageEngine, func(module *Module) { def := module.allDefinitions[testServiceID] @@ -160,7 +160,7 @@ func Test_Module_Register(t *testing.T) { err := m.Register(ctx, testServiceID, otherVP) assert.ErrorIs(t, err, pe.ErrNoCredentials) - _, timestamp, _ := m.Get(ctx, testServiceID, 0) + _, _, timestamp, _ := m.Get(ctx, testServiceID, 0) assert.Equal(t, 0, timestamp) }) t.Run("unsupported DID method", func(t *testing.T) { @@ -184,7 +184,7 @@ func Test_Module_Register(t *testing.T) { Endpoint: "https://example.com/someother", } mockhttpclient := module.httpClient.(*client.MockHTTPClient) - mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", gomock.Any()).Return(nil, 0, nil).AnyTimes() + mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", gomock.Any()).Return(nil, testSeed, 0, nil).AnyTimes() }) ctx := context.WithValue(ctx, XForwardedHostContextKey{}, "https://example.com") @@ -200,7 +200,10 @@ func Test_Module_Register(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} }) t.Run("ok", func(t *testing.T) { - m, testContext := setupModule(t, storageEngine) + m, testContext := setupModule(t, storageEngine, func(module *Module) { + // disable updater + module.config.Client.RefreshInterval = 0 + }) testContext.verifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).Times(2) err := m.Register(ctx, testServiceID, vpAlice) @@ -260,18 +263,18 @@ func Test_Module_Get(t *testing.T) { ctx := context.Background() t.Run("ok", func(t *testing.T) { m, _ := setupModule(t, storageEngine) - require.NoError(t, m.store.add(testServiceID, vpAlice, 0)) - presentations, timestamp, err := m.Get(ctx, testServiceID, 0) + require.NoError(t, m.store.add(testServiceID, vpAlice, testSeed, 0)) + presentations, seed, timestamp, err := m.Get(ctx, testServiceID, 0) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"1": vpAlice}, presentations) assert.Equal(t, 1, timestamp) - }) - t.Run("ok - retrieve delta", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) - require.NoError(t, m.store.add(testServiceID, vpAlice, 0)) - presentations, _, err := m.Get(ctx, testServiceID, 0) - require.NoError(t, err) - require.Len(t, presentations, 1) + assert.NotEmpty(t, seed) + + t.Run("ok - retrieve delta", func(t *testing.T) { + presentations, _, _, err := m.Get(ctx, testServiceID, 1) + require.NoError(t, err) + require.Len(t, presentations, 0) + }) }) t.Run("not a server for this service ID, call forwarded", func(t *testing.T) { m, _ := setupModule(t, storageEngine, func(module *Module) { @@ -280,14 +283,15 @@ func Test_Module_Get(t *testing.T) { Endpoint: "https://example.com/someother", } mockhttpclient := module.httpClient.(*client.MockHTTPClient) - mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, 1, nil).AnyTimes() + mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, "otherSeed", 1, nil).AnyTimes() }) - presentations, timestamp, err := m.Get(ctx, "someother", 0) + presentations, seed, timestamp, err := m.Get(ctx, "someother", 0) require.NoError(t, err) assert.Equal(t, 1, timestamp) assert.Len(t, presentations, 1) + assert.Equal(t, "otherSeed", seed) }) t.Run("not a server for this service ID, call forwarded, cycle detected", func(t *testing.T) { m, _ := setupModule(t, storageEngine, func(module *Module) { @@ -296,11 +300,11 @@ func Test_Module_Get(t *testing.T) { Endpoint: "https://example.com/someother", } mockhttpclient := module.httpClient.(*client.MockHTTPClient) - mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", 0).Return(nil, 0, nil).AnyTimes() + mockhttpclient.EXPECT().Get(gomock.Any(), "https://example.com/someother", 0).Return(nil, "", 0, nil).AnyTimes() }) ctx := context.WithValue(ctx, XForwardedHostContextKey{}, "https://example.com") - _, _, err := m.Get(ctx, "someother", 0) + _, _, _, err := m.Get(ctx, "someother", 0) assert.ErrorIs(t, err, errCyclicForwardingDetected) }) @@ -325,10 +329,23 @@ func setupModule(t *testing.T, storageInstance storage.Engine, visitors ...func( m.config = DefaultConfig() m.publicURL = test.MustParseURL("https://example.com") require.NoError(t, m.Configure(core.TestServerConfig())) + httpClient := client.NewMockHTTPClient(ctrl) - httpClient.EXPECT().Get(gomock.Any(), "http://example.com/other", gomock.Any()).Return(nil, 0, nil).AnyTimes() - httpClient.EXPECT().Get(gomock.Any(), "http://example.com/usecase", gomock.Any()).Return(nil, 0, nil).AnyTimes() - httpClient.EXPECT().Get(gomock.Any(), "http://example.com/unsupported", gomock.Any()).Return(nil, 0, nil).AnyTimes() + httpClient.EXPECT().Get(gomock.Any(), "http://example.com/other", gomock.Any()).Return(nil, testSeed, 0, nil).AnyTimes() + httpClient.EXPECT().Get(gomock.Any(), "http://example.com/usecase", gomock.Any()).Return(nil, testSeed, 0, nil).AnyTimes() + httpClient.EXPECT().Get(gomock.Any(), "http://example.com/unsupported", gomock.Any()).Return(nil, testSeed, 0, nil).AnyTimes() + // set seed in DB otherwise behaviour is unpredictable due to background processes + if m.store != nil { + require.NoError(t, m.store.db.Transaction(func(tx *gorm.DB) error { + service := serviceRecord{ + ID: testServiceID, + Seed: testSeed, + LastLamportTimestamp: 0, + } + return tx.Save(&service).Error + })) + } + m.httpClient = httpClient m.allDefinitions = testDefinitions() m.serverDefinitions = map[string]ServiceDefinition{ @@ -426,7 +443,7 @@ func TestModule_Search(t *testing.T) { t.Run("ok", func(t *testing.T) { m, _ := setupModule(t, storageEngine) - require.NoError(t, m.store.add(testServiceID, vpAlice, 0)) + require.NoError(t, m.store.add(testServiceID, vpAlice, testSeed, 0)) results, err := m.Search(testServiceID, map[string]string{ "credentialSubject.person.givenName": "Alice", @@ -461,7 +478,7 @@ func TestModule_update(t *testing.T) { // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) httpClient := client.NewMockHTTPClient(gomock.NewController(t)) // Get() should be called at least twice (times the number of Service Definitions), once for the initial run on startup, then again after the refresh interval - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, 0, nil).MinTimes(2 * len(module.allDefinitions)) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", 0, nil).MinTimes(2 * len(module.allDefinitions)) module.httpClient = httpClient }) time.Sleep(10 * time.Millisecond) @@ -473,7 +490,7 @@ func TestModule_update(t *testing.T) { // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) httpClient := client.NewMockHTTPClient(gomock.NewController(t)) // update causes call to HttpClient.Get(), once for each Service Definition - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, 0, nil).Times(len(module.allDefinitions)) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", 0, nil).Times(len(module.allDefinitions)) module.httpClient = httpClient }) }) @@ -487,7 +504,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) httpClient := client.NewMockHTTPClient(gomock.NewController(t)) httpClient.EXPECT().Register(gomock.Any(), gomock.Any(), vpAlice).Return(nil) - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, 0, nil) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", 0, nil) module.httpClient = httpClient // disable auto-refresh job to have deterministic assertions module.config.Client.RefreshInterval = 0 @@ -511,7 +528,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) httpClient := client.NewMockHTTPClient(gomock.NewController(t)) httpClient.EXPECT().Register(gomock.Any(), gomock.Any(), vpAlice).Return(nil) - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, 0, nil) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", 0, nil) module.httpClient = httpClient // disable auto-refresh job to have deterministic assertions module.config.Client.RefreshInterval = 0 @@ -614,7 +631,7 @@ func TestModule_GetServiceActivation(t *testing.T) { testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil).AnyTimes() next := time.Now() _ = m.store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &next) - _ = m.store.add(testServiceID, vpAlice, 0) + _ = m.store.add(testServiceID, vpAlice, testSeed, 0) activated, presentation, err := m.GetServiceActivation(context.Background(), testServiceID, aliceSubject) @@ -633,3 +650,13 @@ func TestModule_GetServiceActivation(t *testing.T) { }) }) } + +func checkWriteAccess(dir string) bool { + info, err := os.Stat(dir) + if err != nil { + return false + } + + // Check if the directory is writable by the current user + return info.Mode().Perm()&(1<<(uint(7))) != 0 +} diff --git a/discovery/store.go b/discovery/store.go index d09629185..9859d0b67 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -38,6 +38,7 @@ import ( type serviceRecord struct { ID string `gorm:"primaryKey"` + Seed string LastLamportTimestamp int } @@ -135,7 +136,7 @@ func newSQLStore(db *gorm.DB, clientDefinitions map[string]ServiceDefinition) (* // add adds a presentation to the list of presentations. // If the given timestamp is 0, the server will assign a timestamp. -func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, timestamp int) error { +func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, seed string, timestamp int) error { credentialSubjectID, err := credential.PresentationSigner(presentation) if err != nil { return err @@ -146,13 +147,16 @@ func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, return s.db.Transaction(func(tx *gorm.DB) error { if timestamp == 0 { var newTs *int - newTs, err = s.incrementTimestamp(tx, serviceID) + if len(seed) == 0 { // default for server + seed = uuid.NewString() + } + newTs, err = s.incrementTimestamp(tx, serviceID, seed) if err != nil { return err } timestamp = *newTs } else { - err = s.setTimestamp(tx, serviceID, timestamp) + err = s.setTimestamp(tx, serviceID, seed, timestamp) if err != nil { return err } @@ -202,26 +206,26 @@ func storePresentation(tx *gorm.DB, serviceID string, timestamp int, presentatio // get returns all presentations, registered on the given service, starting after the given timestamp. // It also returns the latest timestamp of the returned presentations. -func (s *sqlStore) get(serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, int, error) { +func (s *sqlStore) get(serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, string, int, error) { var service serviceRecord if err := s.db.Find(&service, "id = ?", serviceID).Error; err != nil { - return nil, 0, fmt.Errorf("query service '%s': %w", serviceID, err) + return nil, "", 0, fmt.Errorf("query service '%s': %w", serviceID, err) } var rows []presentationRecord err := s.db.Order("lamport_timestamp ASC").Find(&rows, "service_id = ? AND lamport_timestamp > ?", serviceID, startAfter).Error if err != nil { - return nil, 0, fmt.Errorf("query service '%s': %w", serviceID, err) + return nil, "", 0, fmt.Errorf("query service '%s': %w", serviceID, err) } presentations := make(map[string]vc.VerifiablePresentation, len(rows)) for _, row := range rows { presentation, err := vc.ParseVerifiablePresentation(row.PresentationRaw) if err != nil { - return nil, 0, fmt.Errorf("parse presentation '%s' of service '%s': %w", row.PresentationID, serviceID, err) + return nil, "", 0, fmt.Errorf("parse presentation '%s' of service '%s': %w", row.PresentationID, serviceID, err) } presentations[fmt.Sprintf("%d", row.LamportTimestamp)] = *presentation } - return presentations, service.LastLamportTimestamp, nil + return presentations, service.Seed, service.LastLamportTimestamp, nil } // search searches for presentations, registered on the given service, matching the given query. @@ -256,14 +260,17 @@ func (s *sqlStore) search(serviceID string, query map[string]string) ([]vc.Verif return results, nil } -// incrementTimestamp increments the last_timestamp of the given service. -func (s *sqlStore) incrementTimestamp(tx *gorm.DB, serviceID string) (*int, error) { +// incrementTimestamp increments the last_timestamp of the given service. USed by server. +func (s *sqlStore) incrementTimestamp(tx *gorm.DB, serviceID string, seed string) (*int, error) { service, err := s.findAndLockService(tx, serviceID) if err != nil { return nil, err } service.ID = serviceID service.LastLamportTimestamp = service.LastLamportTimestamp + 1 + if len(service.Seed) == 0 { // first time this service is used, generate a new testSeed + service.Seed = seed + } if err := tx.Save(service).Error; err != nil { return nil, err @@ -271,14 +278,15 @@ func (s *sqlStore) incrementTimestamp(tx *gorm.DB, serviceID string) (*int, erro return &service.LastLamportTimestamp, nil } -// setTimestamp sets the last_timestamp of the given service. -func (s *sqlStore) setTimestamp(tx *gorm.DB, serviceID string, timestamp int) error { +// setTimestamp sets the last_timestamp of the given service. Used by clients. +func (s *sqlStore) setTimestamp(tx *gorm.DB, serviceID string, seed string, timestamp int) error { service, err := s.findAndLockService(tx, serviceID) if err != nil { return err } service.ID = serviceID service.LastLamportTimestamp = timestamp + service.Seed = seed return tx.Save(service).Error } @@ -496,3 +504,30 @@ func (s *sqlStore) getSubjectVPsOnService(serviceID string, subjectDIDs []did.DI } return result, nil } + +// wipeOnSeedChange wipes the store on a testSeed change. +func (s *sqlStore) wipeOnSeedChange(serviceID string, seed string) error { + return s.db.Transaction(func(tx *gorm.DB) error { + // get the service + service, err := s.findAndLockService(tx, serviceID) + if err != nil { + return err + } + if service.Seed != seed && len(service.Seed) > 0 { + log.Logger(). + WithField("serviceID", serviceID). + Warnf("Seed changed, wiping store (old: %s, new: %s)", service.Seed, seed) + + // wipe the store + if err = tx.Where("service_id = ?", serviceID).Delete(&presentationRecord{}).Error; err != nil { + return err + } + + // reset the testSeed and timestamp + service.Seed = seed + service.LastLamportTimestamp = 0 + return tx.Save(service).Error + } + return nil + }) +} diff --git a/discovery/store_test.go b/discovery/store_test.go index b782cb0a4..812bba212 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -45,21 +45,21 @@ func Test_sqlStore_exists(t *testing.T) { }) t.Run("non-empty list, no match (other subject and ID)", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpBob, 0)) + require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) exists, err := m.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) assert.NoError(t, err) assert.False(t, exists) }) t.Run("non-empty list, no match (other list)", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, 0)) + require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) exists, err := m.exists("other", aliceDID.String(), vpAlice.ID.String()) assert.NoError(t, err) assert.False(t, exists) }) t.Run("non-empty list, match", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, 0)) + require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) exists, err := m.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) assert.NoError(t, err) assert.True(t, exists) @@ -72,13 +72,34 @@ func Test_sqlStore_add(t *testing.T) { t.Run("no credentials in presentation", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - err := m.add(testServiceID, createPresentation(aliceDID), 0) + err := m.add(testServiceID, createPresentation(aliceDID), testSeed, 0) assert.NoError(t, err) }) + t.Run("seed", func(t *testing.T) { + t.Run("passing seed updates last_seed", func(t *testing.T) { + m := setupStore(t, storageEngine.GetSQLDatabase()) + require.NoError(t, m.add(testServiceID, createPresentation(aliceDID), testSeed, 0)) + + _, seed, _, err := m.get(testServiceID, 0) + + require.NoError(t, err) + assert.Equal(t, testSeed, seed) + }) + t.Run("generated seed", func(t *testing.T) { + m := setupStore(t, storageEngine.GetSQLDatabase()) + require.NoError(t, m.add(testServiceID, createPresentation(aliceDID), "", 0)) + + _, seed, _, err := m.get(testServiceID, 0) + + require.NoError(t, err) + assert.Len(t, seed, 36) // uuid v4 + }) + }) + t.Run("passing timestamp updates last_timestamp", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - err := m.add(testServiceID, createPresentation(aliceDID), 1) + err := m.add(testServiceID, createPresentation(aliceDID), testSeed, 1) require.NoError(t, err) timestamp, err := m.getTimestamp(testServiceID) @@ -91,8 +112,8 @@ func Test_sqlStore_add(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) secondVP := createPresentation(aliceDID, vcAlice) - require.NoError(t, m.add(testServiceID, vpAlice, 0)) - require.NoError(t, m.add(testServiceID, secondVP, 0)) + require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + require.NoError(t, m.add(testServiceID, secondVP, testSeed, 0)) // First VP should not exist exists, err := m.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) @@ -112,42 +133,44 @@ func Test_sqlStore_get(t *testing.T) { t.Run("empty list, 0 timestamp", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - presentations, timestamp, err := m.get(testServiceID, 0) + presentations, seed, timestamp, err := m.get(testServiceID, 0) assert.NoError(t, err) assert.Empty(t, presentations) assert.Equal(t, 0, timestamp) + assert.Empty(t, seed) }) t.Run("1 entry, 0 timestamp", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, 0)) - presentations, timestamp, err := m.get(testServiceID, 0) + require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + presentations, seed, timestamp, err := m.get(testServiceID, 0) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"1": vpAlice}, presentations) assert.Equal(t, 1, timestamp) + assert.Equal(t, testSeed, seed) }) t.Run("2 entries, 0 timestamp", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, 0)) - require.NoError(t, m.add(testServiceID, vpBob, 0)) - presentations, timestamp, err := m.get(testServiceID, 0) + require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) + presentations, _, timestamp, err := m.get(testServiceID, 0) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"1": vpAlice, "2": vpBob}, presentations) assert.Equal(t, 2, timestamp) }) t.Run("2 entries, start after first", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, 0)) - require.NoError(t, m.add(testServiceID, vpBob, 0)) - presentations, timestamp, err := m.get(testServiceID, 1) + require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) + presentations, _, timestamp, err := m.get(testServiceID, 1) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"2": vpBob}, presentations) assert.Equal(t, 2, timestamp) }) t.Run("2 entries, start at end", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, 0)) - require.NoError(t, m.add(testServiceID, vpBob, 0)) - presentations, timestamp, err := m.get(testServiceID, 2) + require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) + presentations, _, timestamp, err := m.get(testServiceID, 2) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{}, presentations) assert.Equal(t, 2, timestamp) @@ -159,7 +182,7 @@ func Test_sqlStore_get(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - err := c.add(testServiceID, createPresentation(aliceDID, vcAlice), 0) + err := c.add(testServiceID, createPresentation(aliceDID, vcAlice), testSeed, 0) require.NoError(t, err) }() } @@ -185,7 +208,7 @@ func Test_sqlStore_search(t *testing.T) { vps := []vc.VerifiablePresentation{vpAlice} c := setupStore(t, storageEngine.GetSQLDatabase()) for _, vp := range vps { - err := c.add(testServiceID, vp, 0) + err := c.add(testServiceID, vp, testSeed, 0) require.NoError(t, err) } @@ -200,7 +223,7 @@ func Test_sqlStore_search(t *testing.T) { vps := []vc.VerifiablePresentation{vpAlice, vpBob} c := setupStore(t, storageEngine.GetSQLDatabase()) for _, vp := range vps { - err := c.add(testServiceID, vp, 0) + err := c.add(testServiceID, vp, testSeed, 0) require.NoError(t, err) } @@ -212,7 +235,7 @@ func Test_sqlStore_search(t *testing.T) { vps := []vc.VerifiablePresentation{vpAlice, vpBob} c := setupStore(t, storageEngine.GetSQLDatabase()) for _, vp := range vps { - err := c.add(testServiceID, vp, 0) + err := c.add(testServiceID, vp, testSeed, 0) require.NoError(t, err) } actualVPs, err := c.search(testServiceID, map[string]string{ @@ -350,8 +373,8 @@ func Test_sqlStore_getSubjectVPsOnService(t *testing.T) { _ = storageEngine.Shutdown() }) c := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, c.add(testServiceID, vpAlice2, 0)) - require.NoError(t, c.add(testServiceID, vpBob2, 0)) + require.NoError(t, c.add(testServiceID, vpAlice2, testSeed, 0)) + require.NoError(t, c.add(testServiceID, vpBob2, testSeed, 0)) t.Run("ok - single", func(t *testing.T) { vps, err := c.getSubjectVPsOnService(testServiceID, []did.DID{aliceDID}) @@ -365,6 +388,36 @@ func Test_sqlStore_getSubjectVPsOnService(t *testing.T) { }) } +func Test_sqlStore_wipeOnSeedChange(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + t.Cleanup(func() { + _ = storageEngine.Shutdown() + }) + + t.Run("empty database", func(t *testing.T) { + c := setupStore(t, storageEngine.GetSQLDatabase()) + err := c.wipeOnSeedChange(testServiceID, "other") + require.NoError(t, err) + }) + t.Run("1 entry wiped, 1 remains", func(t *testing.T) { + c := setupStore(t, storageEngine.GetSQLDatabase()) + require.NoError(t, c.add(testServiceID, vpAlice, testSeed, 0)) + require.NoError(t, c.add("other", vpAlice, testSeed, 0)) + + err := c.wipeOnSeedChange(testServiceID, "other") + require.NoError(t, err) + + vps, err := c.search(testServiceID, map[string]string{}) + require.NoError(t, err) + require.Len(t, vps, 0) + vps, err = c.search("other", map[string]string{}) + require.NoError(t, err) + require.Len(t, vps, 1) + }) +} + func setupStore(t *testing.T, db *gorm.DB) *sqlStore { resetStore(t, db) defs := testDefinitions() diff --git a/discovery/test.go b/discovery/test.go index 222f2f9e2..3c927495f 100644 --- a/discovery/test.go +++ b/discovery/test.go @@ -40,6 +40,8 @@ import ( "time" ) +const testSeed = "1234567890" + var keyPairs map[string]*ecdsa.PrivateKey var authorityDID did.DID var aliceSubject string diff --git a/docs/_static/discovery/server.yaml b/docs/_static/discovery/server.yaml index 4bf854784..9e8b8ccbc 100644 --- a/docs/_static/discovery/server.yaml +++ b/docs/_static/discovery/server.yaml @@ -76,9 +76,13 @@ components: PresentationsResponse: type: object required: + - seed - timestamp - entries properties: + seed: + description: unique value for the combination of serviceID and a server instance. + type: string timestamp: description: highest timestamp of the returned presentations, should be used as the timestamp for the next query type: integer diff --git a/storage/sql_migrations/009_discoveryservice_seed.sql b/storage/sql_migrations/009_discoveryservice_seed.sql new file mode 100644 index 000000000..4d0e48a9f --- /dev/null +++ b/storage/sql_migrations/009_discoveryservice_seed.sql @@ -0,0 +1,6 @@ +-- +goose Up +-- discovery_service: add seed column +alter table discovery_service add seed varchar(36); + +-- +goose Down +alter table discovery_service drop column seed; From eee21dd958868ad72121283ee5cab7762524530b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:46:38 +0200 Subject: [PATCH 36/72] Bump github.com/redis/go-redis/v9 from 9.6.1 to 9.6.2 (#3482) Bumps [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) from 9.6.1 to 9.6.2. - [Release notes](https://github.com/redis/go-redis/releases) - [Changelog](https://github.com/redis/go-redis/blob/master/CHANGELOG.md) - [Commits](https://github.com/redis/go-redis/compare/v9.6.1...v9.6.2) --- updated-dependencies: - dependency-name: github.com/redis/go-redis/v9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5c450769a..cd60658a1 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/privacybydesign/irmago v0.16.0 github.com/prometheus/client_golang v1.20.4 github.com/prometheus/client_model v0.6.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index fc966c9e7..653be0bfd 100644 --- a/go.sum +++ b/go.sum @@ -392,8 +392,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk= +github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= From 6b68287f502ffed8140d84d0893ed41f38ab8955 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:02:54 +0200 Subject: [PATCH 37/72] Bump github.com/chromedp/chromedp from 0.10.0 to 0.11.0 (#3485) Bumps [github.com/chromedp/chromedp](https://github.com/chromedp/chromedp) from 0.10.0 to 0.11.0. - [Release notes](https://github.com/chromedp/chromedp/releases) - [Commits](https://github.com/chromedp/chromedp/compare/v0.10.0...v0.11.0) --- updated-dependencies: - dependency-name: github.com/chromedp/chromedp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index cd60658a1..2b4a060fe 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/alicebob/miniredis/v2 v2.33.0 github.com/avast/retry-go/v4 v4.6.0 github.com/cbroglie/mustache v1.4.0 - github.com/chromedp/chromedp v0.10.0 + github.com/chromedp/chromedp v0.11.0 github.com/dlclark/regexp2 v1.11.4 github.com/glebarez/sqlite v1.11.0 github.com/go-redis/redismock/v9 v9.2.0 @@ -83,7 +83,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335 // indirect + github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect diff --git a/go.sum b/go.sum index 653be0bfd..46b8adb38 100644 --- a/go.sum +++ b/go.sum @@ -70,10 +70,10 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335 h1:bATMoZLH2QGct1kzDxfmeBUQI/QhQvB0mBrOTct+YlQ= -github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKab1E= -github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE= +github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df h1:cbtSn19AtqQha1cxmP2Qvgd3fFMz51AeAEKLJMyEUhc= +github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.11.0 h1:1PT6O4g39sBAFjlljIHTpxmCSk8meeYL6+R+oXH4bWA= +github.com/chromedp/chromedp v0.11.0/go.mod h1:jsD7OHrX0Qmskqb5Y4fn4jHnqquqW22rkMFgKbECsqg= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -550,7 +550,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= From 2d811a0182b7b0d0422210445ce8e485340099f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:03:25 +0200 Subject: [PATCH 38/72] Bump github.com/prometheus/client_golang from 1.20.4 to 1.20.5 (#3486) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.4 to 1.20.5. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/v1.20.5/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.20.4...v1.20.5) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2b4a060fe..3051f71bd 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f github.com/pressly/goose/v3 v3.22.0 github.com/privacybydesign/irmago v0.16.0 - github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 github.com/redis/go-redis/v9 v9.6.2 github.com/santhosh-tekuri/jsonschema v1.2.4 diff --git a/go.sum b/go.sum index 46b8adb38..795b027c9 100644 --- a/go.sum +++ b/go.sum @@ -384,8 +384,8 @@ github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750 h1:3RuYOQTlAr github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750/go.mod h1:QZI8hX8Ff2GfZ7UJuxyWw3nAGgt2s5+U4hxY6rmwQvs= github.com/privacybydesign/irmago v0.16.0 h1:PxIPRvpitxfJSocIRwIoYDSETarsopxtByZ5XZ3yzcE= github.com/privacybydesign/irmago v0.16.0/go.mod h1:++4PaFrN8MuRkunja/ID6cX+5KcATi7I0uVG+asI96c= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= From 9bc9b822c8a8225a997e519b200e6f8a6c50424c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:36:31 +0200 Subject: [PATCH 39/72] Bump github.com/Azure/azure-sdk-for-go/sdk/azcore from 1.14.0 to 1.15.0 (#3487) Bumps [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) from 1.14.0 to 1.15.0. - [Release notes](https://github.com/Azure/azure-sdk-for-go/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md) - [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.14.0...sdk/azcore/v1.15.0) --- updated-dependencies: - dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azcore dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3051f71bd..3d2854cc3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/nuts-foundation/nuts-node go 1.23 require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 github.com/alicebob/miniredis/v2 v2.33.0 diff --git a/go.sum b/go.sum index 795b027c9..15fa34fc8 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDr github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0 h1:eXzkOEXbSTOa7cJ7EqeCVi/OFi/ppDrUtQuttCWy74c= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= From 1c34036a22df0fe00425806dc7cad2419f890823 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 16 Oct 2024 08:21:27 +0200 Subject: [PATCH 40/72] add codeowners (#3489) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..56c6f062c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @woutslakhorst @reinkrul @gerardsn @stevenvegt From 0d2e8d73df0f8c867b3a8cbb7d60125419ec940d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:41:36 +0200 Subject: [PATCH 41/72] Bump github.com/Azure/azure-sdk-for-go/sdk/azidentity (#3475) Bumps [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) from 1.7.0 to 1.8.0. - [Release notes](https://github.com/Azure/azure-sdk-for-go/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md) - [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.7.0...sdk/azcore/v1.8.0) --- updated-dependencies: - dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azidentity dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3d2854cc3..bc3f6fc5a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 github.com/alicebob/miniredis/v2 v2.33.0 github.com/avast/retry-go/v4 v4.6.0 diff --git a/go.sum b/go.sum index 15fa34fc8..6658b4e1c 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,10 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0 h1:eXzkOEXbSTOa7cJ7EqeCVi/OFi/ppDrUtQuttCWy74c= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= @@ -20,6 +22,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1: github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= @@ -232,6 +236,8 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= From feb6285951f86b7c1d714106ef6f9bfb9c344149 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:39:29 +0200 Subject: [PATCH 42/72] Bump github.com/pressly/goose/v3 from 3.22.0 to 3.22.1 (#3388) * Bump github.com/pressly/goose/v3 from 3.22.0 to 3.22.1 Bumps [github.com/pressly/goose/v3](https://github.com/pressly/goose) from 3.22.0 to 3.22.1. - [Release notes](https://github.com/pressly/goose/releases) - [Changelog](https://github.com/pressly/goose/blob/master/CHANGELOG.md) - [Commits](https://github.com/pressly/goose/compare/v3.22.0...v3.22.1) --- updated-dependencies: - dependency-name: github.com/pressly/goose/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * update image for circlCI * replace github.com/glebarez/sqlite with local code * select correct dialector * replace copied code with fork --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wout Slakhorst --- .circleci/config.yml | 4 ++-- go.mod | 22 ++++++++++++++-------- go.sum | 26 ++++++++++++-------------- storage/engine.go | 8 +++++--- vcr/revocation/bitstring_test.go | 2 +- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2a4e5c757..202fc79b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: build: parallelism: 8 docker: - - image: cimg/go:1.22 + - image: cimg/go:1.23 steps: - checkout @@ -36,7 +36,7 @@ jobs: report: docker: - - image: cimg/go:1.22 + - image: cimg/go:1.23 steps: - checkout - attach_workspace: diff --git a/go.mod b/go.mod index bc3f6fc5a..eb5442895 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/cbroglie/mustache v1.4.0 github.com/chromedp/chromedp v0.11.0 github.com/dlclark/regexp2 v1.11.4 - github.com/glebarez/sqlite v1.11.0 github.com/go-redis/redismock/v9 v9.2.0 github.com/goodsign/monday v1.0.2 github.com/google/uuid v1.6.0 @@ -34,10 +33,11 @@ require ( github.com/nuts-foundation/go-did v0.14.0 github.com/nuts-foundation/go-leia/v4 v4.0.3 github.com/nuts-foundation/go-stoabs v1.10.0 + github.com/nuts-foundation/sqlite v1.0.0 // check the oapi-codegen tool version in the makefile when upgrading the runtime github.com/oapi-codegen/runtime v1.1.1 github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f - github.com/pressly/goose/v3 v3.22.0 + github.com/pressly/goose/v3 v3.22.1 github.com/privacybydesign/irmago v0.16.0 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 @@ -94,7 +94,6 @@ require ( github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor v1.5.1 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/go-co-op/gocron v1.28.3 // indirect github.com/go-errors/errors v1.4.2 // indirect @@ -123,9 +122,9 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -196,9 +195,16 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 // indirect gorm.io/gorm v1.25.12 - modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.32.0 // indirect + modernc.org/sqlite v1.33.1 rsc.io/qr v0.2.0 // indirect ) + +require ( + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) diff --git a/go.sum b/go.sum index 6658b4e1c..9b9b5e51e 100644 --- a/go.sum +++ b/go.sum @@ -113,10 +113,6 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= -github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= -github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-co-op/gocron v1.28.3 h1:swTsge6u/1Ei51b9VLMz/YTzEzWpbsk5SiR7m5fklTI= @@ -217,12 +213,12 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= @@ -360,6 +356,8 @@ github.com/nuts-foundation/go-leia/v4 v4.0.3 h1:xNZznXWvcIwonXIDmpDDvF7KmP9BOK0M github.com/nuts-foundation/go-leia/v4 v4.0.3/go.mod h1:tYveGED8tSbQYhZNv2DVTc51c2zEWmSF+MG96PAtalY= github.com/nuts-foundation/go-stoabs v1.10.0 h1:mNzm9jgraMc69a8gTgteli8t1CMxr1+gyI7A9Eh0NDk= github.com/nuts-foundation/go-stoabs v1.10.0/go.mod h1:So6S7ninucyJUU7I+JK1zcpoGDsZtd+jLXXacVtSWew= +github.com/nuts-foundation/sqlite v1.0.0 h1:gLKyVIHZqYfYpEy5Ji6vjNUH8rs0luiY3DWNcOSBBzM= +github.com/nuts-foundation/sqlite v1.0.0/go.mod h1:2GHDXCw5Sul9L3h8T5k0+558scm4ol4iXG+wVyYqueI= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= @@ -384,8 +382,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k= github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= -github.com/pressly/goose/v3 v3.22.0 h1:wd/7kNiPTuNAztWun7iaB98DrhulbWPrzMAaw2DEZNw= -github.com/pressly/goose/v3 v3.22.0/go.mod h1:yJM3qwSj2pp7aAaCvso096sguezamNb2OBgxCnh/EYg= +github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc= +github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo= github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750 h1:3RuYOQTlArQ6Uw2TgySusmZGluP+18WdQL56YSfkM3Q= github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750/go.mod h1:QZI8hX8Ff2GfZ7UJuxyWw3nAGgt2s5+U4hxY6rmwQvs= github.com/privacybydesign/irmago v0.16.0 h1:PxIPRvpitxfJSocIRwIoYDSETarsopxtByZ5XZ3yzcE= @@ -640,8 +638,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= -modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/storage/engine.go b/storage/engine.go index c1834217e..3619c7115 100644 --- a/storage/engine.go +++ b/storage/engine.go @@ -29,12 +29,12 @@ import ( "sync" "time" - "github.com/glebarez/sqlite" _ "github.com/microsoft/go-mssqldb/azuread" "github.com/nuts-foundation/go-stoabs" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/storage/log" "github.com/nuts-foundation/nuts-node/storage/sql_migrations" + "github.com/nuts-foundation/sqlite" "github.com/pressly/goose/v3" "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" @@ -42,6 +42,7 @@ import ( "gorm.io/driver/postgres" "gorm.io/driver/sqlserver" "gorm.io/gorm" + _ "modernc.org/sqlite" ) const storeShutdownTimeout = 5 * time.Second @@ -255,8 +256,9 @@ func (e *engine) initSQLDatabase() error { // With 1 connection, all actions will be performed sequentially. This impacts performance, but SQLite should not be used in production. // See https://github.com/nuts-foundation/nuts-node/pull/2589#discussion_r1399130608 db.SetMaxOpenConns(1) - dialector := sqlite.Dialector{Conn: db} - e.sqlDB, err = gorm.Open(dialector, gormConfig) + e.sqlDB, err = gorm.Open(sqlite.Dialector{ + Conn: db, + }, gormConfig) if err != nil { return err } diff --git a/vcr/revocation/bitstring_test.go b/vcr/revocation/bitstring_test.go index 2de832f32..2af0dd08a 100644 --- a/vcr/revocation/bitstring_test.go +++ b/vcr/revocation/bitstring_test.go @@ -20,7 +20,7 @@ package revocation import ( "database/sql" - "github.com/glebarez/sqlite" + "github.com/nuts-foundation/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "math/rand" From 7f2e6dd71a7317f5de73d45de1f7b14d8d49db7d Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 16 Oct 2024 11:53:13 +0200 Subject: [PATCH 43/72] move discovery VP validation to background process to prevent errors (#3483) --- discovery/client.go | 98 +++++++++++- discovery/client_test.go | 147 ++++++++++++++++-- discovery/module.go | 47 ++++-- discovery/module_test.go | 125 +++++++++++---- discovery/store.go | 91 +++++++++-- discovery/store_test.go | 142 +++++++++++++---- .../010_discoverypresentation_validation.sql | 6 + 7 files changed, 550 insertions(+), 106 deletions(-) create mode 100644 storage/sql_migrations/010_discoverypresentation_validation.sql diff --git a/discovery/client.go b/discovery/client.go index e22d4b351..08fc8a4d6 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -33,6 +33,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" + "github.com/nuts-foundation/nuts-node/vcr/types" "github.com/nuts-foundation/nuts-node/vdr/didsubject" "github.com/nuts-foundation/nuts-node/vdr/resolver" "slices" @@ -47,6 +48,10 @@ type clientRegistrationManager interface { deactivate(ctx context.Context, serviceID, subjectID string) error // refresh checks which Verifiable Presentations that are about to expire, and should be refreshed on the Discovery Service. refresh(ctx context.Context, now time.Time) error + // validate validates all presentations that are not yet validated + validate() error + // removeRevoked removes all revoked presentations from the store + removeRevoked() error } var _ clientRegistrationManager = &defaultClientRegistrationManager{} @@ -58,9 +63,10 @@ type defaultClientRegistrationManager struct { vcr vcr.VCR subjectManager didsubject.Manager didResolver resolver.DIDResolver + verifier presentationVerifier } -func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver) *defaultClientRegistrationManager { +func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver, verifier presentationVerifier) *defaultClientRegistrationManager { return &defaultClientRegistrationManager{ services: services, store: store, @@ -68,6 +74,7 @@ func newRegistrationManager(services map[string]ServiceDefinition, store *sqlSto vcr: vcr, subjectManager: subjectManager, didResolver: didResolver, + verifier: verifier, } } @@ -330,6 +337,68 @@ func (r *defaultClientRegistrationManager) refresh(ctx context.Context, now time return nil } +func (r *defaultClientRegistrationManager) validate() error { + errMsg := "background verification of presentation failed (service: %s, id: %s)" + // find all unvalidated entries in store + presentations, err := r.store.allPresentations(false) + if err != nil { + return err + } + j := 0 + for i, presentation := range presentations { + verifiablePresentation, err := vc.ParseVerifiablePresentation(presentation.PresentationRaw) + if err != nil { + log.Logger().WithError(err).Warnf(errMsg, presentation.ServiceID, presentation.ID) + continue + } + service, exists := r.services[presentation.ServiceID] + if !exists { + log.Logger().WithError(err).Warnf("service not found for background validation: %s", presentation.ServiceID) + continue + } + if err = r.verifier(service, *verifiablePresentation); err != nil { + log.Logger().WithError(err).Warnf(errMsg, presentation.ServiceID, presentation.ID) + continue + } + presentations[j] = presentations[i] + j++ + } + // update flag in DB + if j > 0 { + return r.store.updateValidated(presentations[:j]) + } + return nil +} + +func (r *defaultClientRegistrationManager) removeRevoked() error { + errMsg := "background revocation check of presentation failed (id: %s)" + // find all validated entries in store + presentations, err := r.store.allPresentations(true) + if err != nil { + return err + } + + for _, presentation := range presentations { + verifiablePresentation, err := vc.ParseVerifiablePresentation(presentation.PresentationRaw) + if err != nil { + log.Logger().WithError(err).Warnf(errMsg, presentation.ID) + continue + } + _, err = r.vcr.Verifier().VerifyVP(*verifiablePresentation, true, true, nil) + if err != nil && !errors.Is(err, types.ErrRevoked) { + log.Logger().WithError(err).Warnf(errMsg, presentation.ID) + continue + } + if errors.Is(err, types.ErrRevoked) { + log.Logger().WithError(err).Infof("removing revoked presentation (id: %s)", presentation.ID) + if err = r.store.deletePresentationRecord(presentation.ID); err != nil { + log.Logger().WithError(err).Warnf("failed to remove revoked presentation from discovery service (id: %s)", presentation.ID) + } + } + } + return nil +} + // clientUpdater is responsible for updating the local copy of Discovery Services // Callers should only call update(). type clientUpdater struct { @@ -377,13 +446,34 @@ func (u *clientUpdater) updateService(ctx context.Context, service ServiceDefini return fmt.Errorf("failed to wipe on testSeed change (service=%s, testSeed=%s): %w", service.ID, seed, err) } for _, presentation := range presentations { - if err := u.verifier(service, presentation); err != nil { - log.Logger().WithError(err).Warnf("Presentation verification failed, not adding it (service=%s, id=%s)", service.ID, presentation.ID) + // Check if the presentation already exists + credentialSubjectID, err := credential.PresentationSigner(presentation) + if err != nil { + return err + } + exists, err := u.store.exists(service.ID, credentialSubjectID.String(), presentation.ID.String()) + if err != nil { + return err + } + if exists { continue } - if err := u.store.add(service.ID, presentation, seed, serverTimestamp); err != nil { + + // always add the presentation, even if it's not valid + // it won't be returned in a search if invalid + // the validator will set the validated flag to true when it's valid + // it'll also remove it from the store if it's invalidated later + if record, err := u.store.add(service.ID, presentation, seed, serverTimestamp); err != nil { return fmt.Errorf("failed to store presentation (service=%s, id=%s): %w", service.ID, presentation.ID, err) + } else if err = u.verifier(service, presentation); err == nil { + // valid, immediately activate + if err = u.store.updateValidated([]presentationRecord{*record}); err != nil { + return fmt.Errorf("failed to update validated flag (service=%s, id=%s): %w", service.ID, presentation.ID, err) + } + } else { + log.Logger().WithError(err).Infof("failed to verify added presentation (service=%s, id=%s)", service.ID, presentation.ID) } + log.Logger(). WithField("discoveryService", service.ID). WithField("presentationID", presentation.ID). diff --git a/discovery/client_test.go b/discovery/client_test.go index 0ab14a8c4..b3209a43a 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -31,6 +31,8 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vcr/pe" + "github.com/nuts-foundation/nuts-node/vcr/types" + "github.com/nuts-foundation/nuts-node/vcr/verifier" "github.com/nuts-foundation/nuts-node/vdr/didsubject" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" @@ -64,7 +66,7 @@ func newTestContext(t *testing.T) testContext { wallet := holder.NewMockWallet(ctrl) subjectManager := didsubject.NewMockManager(ctrl) store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, vcr, subjectManager, didResolver) + manager := newRegistrationManager(testDefinitions(), store, invoker, vcr, subjectManager, didResolver, alwaysOkVerifier) vcr.EXPECT().Wallet().Return(wallet).AnyTimes() return testContext{ @@ -181,7 +183,7 @@ func Test_defaultClientRegistrationManager_activate(t *testing.T) { return &vpAlice, nil }) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - ctx.manager = newRegistrationManager(emptyDefinition, ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver) + ctx.manager = newRegistrationManager(emptyDefinition, ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver, alwaysOkVerifier) err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) @@ -221,9 +223,10 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()) ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1)) + _, err := ctx.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) - err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err = ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) @@ -236,9 +239,10 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { claims["retract_jti"] = vpAlice.ID.String() vp.Type = append(vp.Type, retractionPresentationType) }, vcAlice) - require.NoError(t, ctx.store.add(testServiceID, vpAliceDeactivated, testSeed, 1)) + _, err := ctx.store.add(testServiceID, vpAliceDeactivated, testSeed, 1) + require.NoError(t, err) - err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err = ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) @@ -255,9 +259,10 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")) ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1)) + _, err := ctx.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) - err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err = ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "remote error") @@ -266,9 +271,10 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { ctx := newTestContext(t) ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil, assert.AnError) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1)) + _, err := ctx.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) - err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err = ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.ErrorIs(t, err, assert.AnError) }) @@ -380,6 +386,104 @@ func Test_defaultClientRegistrationManager_refresh(t *testing.T) { }) } +func Test_defaultClientRegistrationManager_validate(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + + tests := []struct { + name string + setupManager func(ctx testContext) *defaultClientRegistrationManager + expectedLen int + }{ + { + name: "ok", + setupManager: func(ctx testContext) *defaultClientRegistrationManager { + return ctx.manager + }, + expectedLen: 1, + }, + { + name: "verification failed", + setupManager: func(ctx testContext) *defaultClientRegistrationManager { + return newRegistrationManager(testDefinitions(), ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver, func(service ServiceDefinition, vp vc.VerifiablePresentation) error { + return errors.New("verification failed") + }) + }, + expectedLen: 0, + }, + { + name: "registration for unknown service", + setupManager: func(ctx testContext) *defaultClientRegistrationManager { + return newRegistrationManager(map[string]ServiceDefinition{}, ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver, alwaysOkVerifier) + }, + expectedLen: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := newTestContext(t) + _, err := ctx.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) + manager := tt.setupManager(ctx) + + err = manager.validate() + require.NoError(t, err) + + presentations, err := ctx.store.allPresentations(true) + require.NoError(t, err) + assert.Len(t, presentations, tt.expectedLen) + }) + } +} + +func Test_defaultClientRegistrationManager_removeRevoked(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + + tests := []struct { + name string + verifyVPError error + expectedLen int + }{ + { + name: "ok - not revoked", + verifyVPError: nil, + expectedLen: 1, + }, + { + name: "ok - revoked", + verifyVPError: types.ErrRevoked, + expectedLen: 0, + }, + { + name: "error", + verifyVPError: assert.AnError, + expectedLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := newTestContext(t) + _, err := ctx.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) + require.NoError(t, ctx.manager.validate()) + + mockVerifier := verifier.NewMockVerifier(ctx.ctrl) + ctx.vcr.EXPECT().Verifier().Return(mockVerifier).AnyTimes() + mockVerifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).Return(nil, tt.verifyVPError) + + err = ctx.manager.removeRevoked() + require.NoError(t, err) + + presentations, err := ctx.store.allPresentations(true) + require.NoError(t, err) + assert.Len(t, presentations, tt.expectedLen) + }) + } +} + func Test_clientUpdater_updateService(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) @@ -408,11 +512,21 @@ func Test_clientUpdater_updateService(t *testing.T) { httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, testSeed, 1, nil) - err := updater.updateService(ctx, testDefinitions()[testServiceID]) + require.NoError(t, updater.updateService(ctx, testDefinitions()[testServiceID])) - require.NoError(t, err) + t.Run("ignores duplicates", func(t *testing.T) { + httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 1).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, testSeed, 1, nil) + + require.NoError(t, updater.updateService(ctx, testDefinitions()[testServiceID])) + + // check count + presentation, err := updater.store.allPresentations(true) + + require.NoError(t, err) + assert.Len(t, presentation, 1) + }) }) - t.Run("ignores invalid presentations", func(t *testing.T) { + t.Run("allows invalid presentations", func(t *testing.T) { resetStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) httpClient := client.NewMockHTTPClient(ctrl) @@ -428,13 +542,16 @@ func Test_clientUpdater_updateService(t *testing.T) { err := updater.updateService(ctx, testDefinitions()[testServiceID]) require.NoError(t, err) - // Bob's VP should exist, Alice's not + // Both should exist, 1 should be validated immediately exists, err := store.exists(testServiceID, bobDID.String(), vpBob.ID.String()) require.NoError(t, err) require.True(t, exists) exists, err = store.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) require.NoError(t, err) - require.False(t, exists) + require.True(t, exists) + validated, err := store.allPresentations(true) + require.NoError(t, err) + require.Len(t, validated, 1) }) t.Run("pass timestamp", func(t *testing.T) { resetStore(t, storageEngine.GetSQLDatabase()) diff --git a/discovery/module.go b/discovery/module.go index a34f45fed..a231d47ab 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -152,7 +152,7 @@ func (m *Module) Start() error { return err } m.clientUpdater = newClientUpdater(m.allDefinitions, m.store, m.verifyRegistration, m.httpClient) - m.registrationManager = newRegistrationManager(m.allDefinitions, m.store, m.httpClient, m.vcrInstance, m.subjectManager, m.didResolver) + m.registrationManager = newRegistrationManager(m.allDefinitions, m.store, m.httpClient, m.vcrInstance, m.subjectManager, m.didResolver, m.verifyRegistration) if m.config.Client.RefreshInterval > 0 { m.routines.Add(1) go func() { @@ -203,7 +203,28 @@ func (m *Module) Register(context context.Context, serviceID string, presentatio return err } - return m.store.add(serviceID, presentation, "", 0) + // Check if the presentation already exists + credentialSubjectID, err := credential.PresentationSigner(presentation) + if err != nil { + return err + } + exists, err := m.store.exists(definition.ID, credentialSubjectID.String(), presentation.ID.String()) + if err != nil { + return err + } + if exists { + return errors.Join(ErrInvalidPresentation, ErrPresentationAlreadyExists) + } + record, err := m.store.add(serviceID, presentation, "", 0) + if err != nil { + return err + } + // also update validated flag since validation is already done + if err = m.store.updateValidated([]presentationRecord{*record}); err != nil { + log.Logger().WithError(err).Errorf("failed to update validated flag for presentation (id: %s)", record.ID) + } + + return nil } func (m *Module) verifyRegistration(definition ServiceDefinition, presentation vc.VerifiablePresentation) error { @@ -235,15 +256,7 @@ func (m *Module) verifyRegistration(definition ServiceDefinition, presentation v return errors.Join(ErrInvalidPresentation, ErrDIDMethodsNotSupported) } - // Check if the presentation already exists - exists, err := m.store.exists(definition.ID, credentialSubjectID.String(), presentation.ID.String()) - if err != nil { - return err - } - if exists { - return errors.Join(ErrInvalidPresentation, ErrPresentationAlreadyExists) - } - // Depending on the presentation type, we need to validate different properties before storing it. + // Depending on the presentation type, we need to updateValidated different properties before storing it. if presentation.IsType(retractionPresentationType) { err = m.validateRetraction(definition.ID, presentation) } else { @@ -484,7 +497,7 @@ func (m *Module) Search(serviceID string, query map[string]string) ([]SearchResu if !exists { return nil, ErrServiceNotFound } - matchingVPs, err := m.store.search(serviceID, query) + matchingVPs, err := m.store.search(serviceID, query, false) if err != nil { return nil, err } @@ -557,6 +570,16 @@ func (m *Module) update() { if err != nil { log.Logger().WithError(err).Errorf("Failed to load latest Verifiable Presentations from Discovery Service") } + // updateValidated all presentations not yet validated + err = m.registrationManager.validate() + if err != nil { + log.Logger().WithError(err).Errorf("Failed to validate presentations") + } + // purge list + err = m.registrationManager.removeRevoked() + if err != nil { + log.Logger().WithError(err).Errorf("Failed to remove revoked presentations") + } } do() for { diff --git a/discovery/module_test.go b/discovery/module_test.go index 23f5f436e..f0569a9fc 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -41,6 +41,8 @@ import ( "go.uber.org/mock/gomock" "gorm.io/gorm" "os" + "sync" + "sync/atomic" "testing" "time" ) @@ -61,8 +63,10 @@ func Test_Module_Register(t *testing.T) { t.Run("registration", func(t *testing.T) { t.Run("ok", func(t *testing.T) { - m, testContext := setupModule(t, storageEngine) - testContext.verifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil) + m, testContext := setupModule(t, storageEngine, func(module *Module) { + module.config.Client.RefreshInterval = 0 + }) + testContext.verifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).Times(2) err := m.Register(ctx, testServiceID, vpAlice) require.NoError(t, err) @@ -262,8 +266,11 @@ func Test_Module_Get(t *testing.T) { require.NoError(t, storageEngine.Start()) ctx := context.Background() t.Run("ok", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) - require.NoError(t, m.store.add(testServiceID, vpAlice, testSeed, 0)) + m, _ := setupModule(t, storageEngine, func(module *Module) { + module.config.Client.RefreshInterval = 0 + }) + _, err := m.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) presentations, seed, timestamp, err := m.Get(ctx, testServiceID, 0) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"1": vpAlice}, presentations) @@ -441,9 +448,13 @@ func TestModule_Search(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) t.Run("ok", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) - - require.NoError(t, m.store.add(testServiceID, vpAlice, testSeed, 0)) + m, ctx := setupModule(t, storageEngine, func(module *Module) { + module.config.Client.RefreshInterval = 0 + }) + ctx.verifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil) + _, err := m.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) + require.NoError(t, m.registrationManager.validate()) results, err := m.Search(testServiceID, map[string]string{ "credentialSubject.person.givenName": "Alice", @@ -471,29 +482,78 @@ func TestModule_Search(t *testing.T) { func TestModule_update(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) - t.Run("Start() initiates update", func(t *testing.T) { - _, _ = setupModule(t, storageEngine, func(module *Module) { - // we want to assert the job runs, so make it run very often to make the test faster - module.config.Client.RefreshInterval = 1 * time.Millisecond - // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) - httpClient := client.NewMockHTTPClient(gomock.NewController(t)) - // Get() should be called at least twice (times the number of Service Definitions), once for the initial run on startup, then again after the refresh interval - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", 0, nil).MinTimes(2 * len(module.allDefinitions)) - module.httpClient = httpClient - }) - time.Sleep(10 * time.Millisecond) - }) - t.Run("update() runs on node startup", func(t *testing.T) { - _, _ = setupModule(t, storageEngine, func(module *Module) { - // we want to assert the job immediately executes on node startup, even if the refresh interval hasn't passed - module.config.Client.RefreshInterval = time.Hour - // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) - httpClient := client.NewMockHTTPClient(gomock.NewController(t)) - // update causes call to HttpClient.Get(), once for each Service Definition - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", 0, nil).Times(len(module.allDefinitions)) - module.httpClient = httpClient + + tests := []struct { + name string + refreshInterval time.Duration + expectedHTTPCalls int + expectedVerifyVPCalls int + }{ + { + name: "Start() initiates update", + refreshInterval: time.Millisecond, + expectedHTTPCalls: 2, + expectedVerifyVPCalls: 4, + }, + { + name: "update() runs on node startup", + refreshInterval: time.Hour, + expectedHTTPCalls: 1, + expectedVerifyVPCalls: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetStore(t, storageEngine.GetSQLDatabase()) + ctrl := gomock.NewController(t) + mockVerifier := verifier.NewMockVerifier(ctrl) + mockVCR := vcr.NewMockVCR(ctrl) + mockVCR.EXPECT().Verifier().Return(mockVerifier).AnyTimes() + m := New(storageEngine, mockVCR, nil, nil) + m.config = DefaultConfig() + m.publicURL = test.MustParseURL("https://example.com") + m.config.Client.RefreshInterval = tt.refreshInterval + require.NoError(t, m.Configure(core.TestServerConfig())) + m.allDefinitions = testDefinitions() + httpClient := client.NewMockHTTPClient(ctrl) + httpWg := sync.WaitGroup{} + httpWg.Add(tt.expectedHTTPCalls * len(m.allDefinitions)) + httpCounter := atomic.Int64{} + httpCounter.Add(int64(tt.expectedHTTPCalls * len(m.allDefinitions))) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _, _ interface{}) (map[string]vc.VerifiablePresentation, string, int, error) { + if httpCounter.Load() != int64(0) { + httpWg.Done() + httpCounter.Add(int64(-1)) + } + return nil, testSeed, 0, nil + }).MinTimes(tt.expectedHTTPCalls * len(m.allDefinitions)) + m.httpClient = httpClient + m.store, _ = newSQLStore(m.storageInstance.GetSQLDatabase(), m.allDefinitions) + vpWg := sync.WaitGroup{} + vpWg.Add(tt.expectedVerifyVPCalls) + vpCounter := atomic.Int64{} + vpCounter.Add(int64(tt.expectedVerifyVPCalls)) + mockVerifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).DoAndReturn(func(_, _, _, _ interface{}) ([]vc.VerifiableCredential, error) { + if vpCounter.Load() != int64(0) { + vpWg.Done() + vpCounter.Add(int64(-1)) + } + return nil, nil + }).MinTimes(tt.expectedVerifyVPCalls) + _, err := m.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) + + require.NoError(t, m.Start()) + + vpWg.Wait() + httpWg.Wait() + + t.Cleanup(func() { + _ = m.Shutdown() + }) }) - }) + } } func TestModule_ActivateServiceForSubject(t *testing.T) { @@ -627,11 +687,14 @@ func TestModule_GetServiceActivation(t *testing.T) { assert.Nil(t, presentation) }) t.Run("activated, with VP", func(t *testing.T) { - m, testContext := setupModule(t, storageEngine) + m, testContext := setupModule(t, storageEngine, func(module *Module) { + module.config.Client.RefreshInterval = 0 + }) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil).AnyTimes() next := time.Now() _ = m.store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &next) - _ = m.store.add(testServiceID, vpAlice, testSeed, 0) + _, err := m.store.add(testServiceID, vpAlice, testSeed, 1) + require.NoError(t, err) activated, presentation, err := m.GetServiceActivation(context.Background(), testServiceID, aliceSubject) diff --git a/discovery/store.go b/discovery/store.go index 9859d0b67..9f7ae7e79 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -19,6 +19,7 @@ package discovery import ( + "database/sql/driver" "encoding/json" "errors" "fmt" @@ -48,6 +49,8 @@ func (s serviceRecord) TableName() string { var _ schema.Tabler = (*presentationRecord)(nil) +type SQLBool bool + type presentationRecord struct { ID string `gorm:"primaryKey"` ServiceID string @@ -56,6 +59,7 @@ type presentationRecord struct { PresentationID string PresentationRaw string PresentationExpiration int64 + Validated SQLBool Credentials []credentialRecord `gorm:"foreignKey:PresentationID;references:ID"` } @@ -63,6 +67,30 @@ func (s presentationRecord) TableName() string { return "discovery_presentation" } +func (b *SQLBool) Scan(value interface{}) error { + *b = false + if value != nil { + switch v := value.(type) { + case int64: + if v != 0 { + *b = true + } + } + } + return nil +} + +func (b SQLBool) Value() (driver.Value, error) { + if b { + return int64(1), nil + } + return int64(0), nil +} + +func (b SQLBool) Bool() bool { + return bool(b) +} + // credentialRecord is a Verifiable Credential, part of a presentation (entry) on a use case list. type credentialRecord struct { // ID is the unique identifier of the entry. @@ -136,15 +164,16 @@ func newSQLStore(db *gorm.DB, clientDefinitions map[string]ServiceDefinition) (* // add adds a presentation to the list of presentations. // If the given timestamp is 0, the server will assign a timestamp. -func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, seed string, timestamp int) error { +func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, seed string, timestamp int) (*presentationRecord, error) { credentialSubjectID, err := credential.PresentationSigner(presentation) if err != nil { - return err + return nil, err } if err := s.prune(); err != nil { - return err + return nil, err } - return s.db.Transaction(func(tx *gorm.DB) error { + var newPresentation *presentationRecord + return newPresentation, s.db.Transaction(func(tx *gorm.DB) error { if timestamp == 0 { var newTs *int if len(seed) == 0 { // default for server @@ -167,15 +196,16 @@ func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, return err } - return storePresentation(tx, serviceID, timestamp, presentation) + newPresentation, err = storePresentation(tx, serviceID, timestamp, presentation) + return err }) } // storePresentation creates a presentationRecord from a VerifiablePresentation and stores it, with its credentials, in the database. -func storePresentation(tx *gorm.DB, serviceID string, timestamp int, presentation vc.VerifiablePresentation) error { +func storePresentation(tx *gorm.DB, serviceID string, timestamp int, presentation vc.VerifiablePresentation) (*presentationRecord, error) { credentialSubjectID, err := credential.PresentationSigner(presentation) if err != nil { - return err + return nil, err } newPresentation := presentationRecord{ @@ -192,7 +222,7 @@ func storePresentation(tx *gorm.DB, serviceID string, timestamp int, presentatio for _, verifiableCredential := range presentation.VerifiableCredential { cred, err := credentialStore.Store(tx, verifiableCredential) if err != nil { - return err + return nil, err } newPresentation.Credentials = append(newPresentation.Credentials, credentialRecord{ ID: uuid.NewString(), @@ -201,7 +231,8 @@ func storePresentation(tx *gorm.DB, serviceID string, timestamp int, presentatio }) } - return tx.Create(&newPresentation).Error + err = tx.Create(&newPresentation).Error + return &newPresentation, err } // get returns all presentations, registered on the given service, starting after the given timestamp. @@ -232,11 +263,14 @@ func (s *sqlStore) get(serviceID string, startAfter int) (map[string]vc.Verifiab // The query is a map of JSON paths and expected string values, matched against the presentation's credentials. // Wildcard matching is supported by prefixing or suffixing the value with an asterisk (*). // It returns the presentations which contain credentials that match the given query. -func (s *sqlStore) search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) { +func (s *sqlStore) search(serviceID string, query map[string]string, allowUnvalidated bool) ([]vc.VerifiablePresentation, error) { // first only select columns also used in group by clause // if the query is empty, there's no need to do a join stmt := s.db.Model(&presentationRecord{}). Where("service_id = ?", serviceID) + if !allowUnvalidated { + stmt = stmt.Where("validated != 0") + } if len(query) > 0 { stmt = stmt.Joins("inner join discovery_credential ON discovery_credential.presentation_id = discovery_presentation.id") stmt = store.CredentialStore{}.BuildSearchStatement(stmt, "discovery_credential.credential_id", query) @@ -344,6 +378,41 @@ func (s *sqlStore) removeExpired() (int, error) { return int(result.RowsAffected), nil } +// allPresentations returns all presentations, the validated param can be used to select validated or unvalidated presentations +func (s *sqlStore) allPresentations(validated bool) ([]presentationRecord, error) { + result := make([]presentationRecord, 0) + stmt := s.db + if validated { + stmt = stmt.Where("validated != 0") + } else { + stmt = stmt.Where("validated = 0") + } + err := stmt.Find(&result).Error + if err != nil { + return nil, err + } + return result, nil +} + +// updateValidated sets the validated flag for the given presentations +func (s *sqlStore) updateValidated(records []presentationRecord) error { + return s.db.Transaction(func(tx *gorm.DB) error { + for _, record := range records { + if err := tx.Model(&presentationRecord{}).Where("id = ?", record.ID).Update("validated", true).Error; err != nil { + return err + } + } + return nil + }) +} + +// deletePresentationRecord removes a presentationRecord from the store based on its ID +func (s *sqlStore) deletePresentationRecord(id string) error { + return s.db.Transaction(func(tx *gorm.DB) error { + return tx.Delete(&presentationRecord{}, "id = ?", id).Error + }) +} + // updatePresentationRefreshTime creates/updates the next refresh time for a Verifiable Presentation on a Discovery Service. // If nextRegistration is nil, the entry will be removed from the database. func (s *sqlStore) updatePresentationRefreshTime(serviceID string, subjectID string, parameters map[string]interface{}, nextRefresh *time.Time) error { @@ -466,7 +535,7 @@ func (s *sqlStore) getSubjectVPsOnService(serviceID string, subjectDIDs []did.DI for _, subjectDID := range subjectDIDs { loopVPs, err := s.search(serviceID, map[string]string{ "credentialSubject.id": subjectDID.String(), - }) + }, true) if err != nil { return nil, err } diff --git a/discovery/store_test.go b/discovery/store_test.go index 812bba212..9daa0c9f6 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -45,21 +45,24 @@ func Test_sqlStore_exists(t *testing.T) { }) t.Run("non-empty list, no match (other subject and ID)", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) + _, err := m.add(testServiceID, vpBob, testSeed, 0) + require.NoError(t, err) exists, err := m.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) assert.NoError(t, err) assert.False(t, exists) }) t.Run("non-empty list, no match (other list)", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + _, err := m.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) exists, err := m.exists("other", aliceDID.String(), vpAlice.ID.String()) assert.NoError(t, err) assert.False(t, exists) }) t.Run("non-empty list, match", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + _, err := m.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) exists, err := m.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) assert.NoError(t, err) assert.True(t, exists) @@ -72,14 +75,15 @@ func Test_sqlStore_add(t *testing.T) { t.Run("no credentials in presentation", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - err := m.add(testServiceID, createPresentation(aliceDID), testSeed, 0) + _, err := m.add(testServiceID, createPresentation(aliceDID), testSeed, 0) assert.NoError(t, err) }) t.Run("seed", func(t *testing.T) { t.Run("passing seed updates last_seed", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, createPresentation(aliceDID), testSeed, 0)) + _, err := m.add(testServiceID, createPresentation(aliceDID), testSeed, 0) + require.NoError(t, err) _, seed, _, err := m.get(testServiceID, 0) @@ -88,7 +92,8 @@ func Test_sqlStore_add(t *testing.T) { }) t.Run("generated seed", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, createPresentation(aliceDID), "", 0)) + _, err := m.add(testServiceID, createPresentation(aliceDID), "", 0) + require.NoError(t, err) _, seed, _, err := m.get(testServiceID, 0) @@ -99,7 +104,7 @@ func Test_sqlStore_add(t *testing.T) { t.Run("passing timestamp updates last_timestamp", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - err := m.add(testServiceID, createPresentation(aliceDID), testSeed, 1) + _, err := m.add(testServiceID, createPresentation(aliceDID), testSeed, 1) require.NoError(t, err) timestamp, err := m.getTimestamp(testServiceID) @@ -112,8 +117,10 @@ func Test_sqlStore_add(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) secondVP := createPresentation(aliceDID, vcAlice) - require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) - require.NoError(t, m.add(testServiceID, secondVP, testSeed, 0)) + _, err := m.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) + _, err = m.add(testServiceID, secondVP, testSeed, 0) + require.NoError(t, err) // First VP should not exist exists, err := m.exists(testServiceID, aliceDID.String(), vpAlice.ID.String()) @@ -141,7 +148,8 @@ func Test_sqlStore_get(t *testing.T) { }) t.Run("1 entry, 0 timestamp", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) + _, err := m.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) presentations, seed, timestamp, err := m.get(testServiceID, 0) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"1": vpAlice}, presentations) @@ -150,8 +158,10 @@ func Test_sqlStore_get(t *testing.T) { }) t.Run("2 entries, 0 timestamp", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) - require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) + _, err := m.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) + _, err = m.add(testServiceID, vpBob, testSeed, 0) + require.NoError(t, err) presentations, _, timestamp, err := m.get(testServiceID, 0) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"1": vpAlice, "2": vpBob}, presentations) @@ -159,8 +169,10 @@ func Test_sqlStore_get(t *testing.T) { }) t.Run("2 entries, start after first", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) - require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) + _, err := m.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) + _, err = m.add(testServiceID, vpBob, testSeed, 0) + require.NoError(t, err) presentations, _, timestamp, err := m.get(testServiceID, 1) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{"2": vpBob}, presentations) @@ -168,8 +180,9 @@ func Test_sqlStore_get(t *testing.T) { }) t.Run("2 entries, start at end", func(t *testing.T) { m := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, m.add(testServiceID, vpAlice, testSeed, 0)) - require.NoError(t, m.add(testServiceID, vpBob, testSeed, 0)) + _, err := m.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) + _, err = m.add(testServiceID, vpBob, testSeed, 0) presentations, _, timestamp, err := m.get(testServiceID, 2) assert.NoError(t, err) assert.Equal(t, map[string]vc.VerifiablePresentation{}, presentations) @@ -182,7 +195,7 @@ func Test_sqlStore_get(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - err := c.add(testServiceID, createPresentation(aliceDID, vcAlice), testSeed, 0) + _, err := c.add(testServiceID, createPresentation(aliceDID, vcAlice), testSeed, 0) require.NoError(t, err) }() } @@ -200,7 +213,7 @@ func Test_sqlStore_search(t *testing.T) { t.Run("empty database", func(t *testing.T) { c := setupStore(t, storageEngine.GetSQLDatabase()) - actualVPs, err := c.search(testServiceID, map[string]string{}) + actualVPs, err := c.search(testServiceID, map[string]string{}, true) require.NoError(t, err) require.Len(t, actualVPs, 0) }) @@ -208,13 +221,13 @@ func Test_sqlStore_search(t *testing.T) { vps := []vc.VerifiablePresentation{vpAlice} c := setupStore(t, storageEngine.GetSQLDatabase()) for _, vp := range vps { - err := c.add(testServiceID, vp, testSeed, 0) + _, err := c.add(testServiceID, vp, testSeed, 0) require.NoError(t, err) } actualVPs, err := c.search(testServiceID, map[string]string{ "credentialSubject.person.givenName": "Alice", - }) + }, true) require.NoError(t, err) require.Len(t, actualVPs, 1) assert.Equal(t, vpAlice.ID.String(), actualVPs[0].ID.String()) @@ -223,24 +236,30 @@ func Test_sqlStore_search(t *testing.T) { vps := []vc.VerifiablePresentation{vpAlice, vpBob} c := setupStore(t, storageEngine.GetSQLDatabase()) for _, vp := range vps { - err := c.add(testServiceID, vp, testSeed, 0) + _, err := c.add(testServiceID, vp, testSeed, 0) require.NoError(t, err) } - actualVPs, err := c.search(testServiceID, map[string]string{}) + actualVPs, err := c.search(testServiceID, map[string]string{}, true) require.NoError(t, err) require.Len(t, actualVPs, 2) + + t.Run("validated", func(t *testing.T) { + actualVPs, err = c.search(testServiceID, map[string]string{}, false) + require.NoError(t, err) + require.Len(t, actualVPs, 0) + }) }) t.Run("not found", func(t *testing.T) { vps := []vc.VerifiablePresentation{vpAlice, vpBob} c := setupStore(t, storageEngine.GetSQLDatabase()) for _, vp := range vps { - err := c.add(testServiceID, vp, testSeed, 0) + _, err := c.add(testServiceID, vp, testSeed, 0) require.NoError(t, err) } actualVPs, err := c.search(testServiceID, map[string]string{ "credentialSubject.person.givenName": "Charlie", - }) + }, true) require.NoError(t, err) require.Len(t, actualVPs, 0) }) @@ -345,7 +364,7 @@ func Test_sqlStore_setPresentationRefreshError(t *testing.T) { assert.Equal(t, refreshError.Error, assert.AnError.Error()) assert.True(t, refreshError.LastOccurrence > int(time.Now().Add(-1*time.Second).Unix())) }) - t.Run("delete", func(t *testing.T) { + t.Run("deletePresentationRecord", func(t *testing.T) { c := setupStore(t, storageEngine.GetSQLDatabase()) require.NoError(t, c.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, to.Ptr(time.Now().Add(time.Second)))) require.NoError(t, c.setPresentationRefreshError(testServiceID, aliceSubject, assert.AnError)) @@ -373,8 +392,10 @@ func Test_sqlStore_getSubjectVPsOnService(t *testing.T) { _ = storageEngine.Shutdown() }) c := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, c.add(testServiceID, vpAlice2, testSeed, 0)) - require.NoError(t, c.add(testServiceID, vpBob2, testSeed, 0)) + _, err := c.add(testServiceID, vpAlice2, testSeed, 0) + require.NoError(t, err) + _, err = c.add(testServiceID, vpBob2, testSeed, 0) + require.NoError(t, err) t.Run("ok - single", func(t *testing.T) { vps, err := c.getSubjectVPsOnService(testServiceID, []did.DID{aliceDID}) @@ -403,21 +424,76 @@ func Test_sqlStore_wipeOnSeedChange(t *testing.T) { }) t.Run("1 entry wiped, 1 remains", func(t *testing.T) { c := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, c.add(testServiceID, vpAlice, testSeed, 0)) - require.NoError(t, c.add("other", vpAlice, testSeed, 0)) + _, err := c.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) + _, err = c.add("other", vpAlice, testSeed, 0) + require.NoError(t, err) - err := c.wipeOnSeedChange(testServiceID, "other") + err = c.wipeOnSeedChange(testServiceID, "other") require.NoError(t, err) - vps, err := c.search(testServiceID, map[string]string{}) + vps, err := c.search(testServiceID, map[string]string{}, true) require.NoError(t, err) require.Len(t, vps, 0) - vps, err = c.search("other", map[string]string{}) + vps, err = c.search("other", map[string]string{}, true) require.NoError(t, err) require.Len(t, vps, 1) }) } +func Test_sqlStore_updateValidated(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + t.Cleanup(func() { + _ = storageEngine.Shutdown() + }) + + c := setupStore(t, storageEngine.GetSQLDatabase()) + _, err := c.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) + + result, err := c.allPresentations(true) + require.NoError(t, err) + assert.Len(t, result, 0) + result, err = c.allPresentations(false) + require.NoError(t, err) + assert.Len(t, result, 1) + + t.Run("validated", func(t *testing.T) { + err = c.updateValidated(result) + require.NoError(t, err) + + result, err = c.allPresentations(false) + require.NoError(t, err) + assert.Len(t, result, 0) + result, err = c.allPresentations(true) + require.NoError(t, err) + assert.Len(t, result, 1) + }) +} + +func Test_sqlStore_delete(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + t.Cleanup(func() { + _ = storageEngine.Shutdown() + }) + + c := setupStore(t, storageEngine.GetSQLDatabase()) + _, err := c.add(testServiceID, vpAlice, testSeed, 0) + require.NoError(t, err) + presentations, _ := c.allPresentations(false) + require.Len(t, presentations, 1) + + err = c.deletePresentationRecord(presentations[0].ID) + + require.NoError(t, err) + + result, err := c.allPresentations(false) + require.NoError(t, err) + assert.Len(t, result, 0) +} + func setupStore(t *testing.T, db *gorm.DB) *sqlStore { resetStore(t, db) defs := testDefinitions() @@ -427,7 +503,7 @@ func setupStore(t *testing.T, db *gorm.DB) *sqlStore { } func resetStore(t *testing.T, db *gorm.DB) { - // related tables are emptied due to on-delete-cascade clause + // related tables are emptied due to on-deletePresentationRecord-cascade clause tableNames := []string{"discovery_service", "discovery_presentation", "discovery_credential", "credential", "credential_prop"} for _, tableName := range tableNames { require.NoError(t, db.Exec("DELETE FROM "+tableName).Error) diff --git a/storage/sql_migrations/010_discoverypresentation_validation.sql b/storage/sql_migrations/010_discoverypresentation_validation.sql new file mode 100644 index 000000000..381a6b20d --- /dev/null +++ b/storage/sql_migrations/010_discoverypresentation_validation.sql @@ -0,0 +1,6 @@ +-- +goose Up +-- discovery_presentation: add validated column +alter table discovery_presentation add validated SMALLINT NOT NULL DEFAULT 0; + +-- +goose Down +alter table discovery_presentation drop column validated; From e9f86eec0f8af4eabb65db6dc44c292497e1d229 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 16 Oct 2024 11:53:39 +0200 Subject: [PATCH 44/72] require DiscoveryRegistrationCredential in PEX (#3484) --- discovery/client.go | 12 ----------- discovery/client_test.go | 3 +-- discovery/module.go | 20 ------------------- discovery/module_test.go | 1 + discovery/test.go | 10 ++++++++++ docs/pages/deployment/discovery.rst | 13 ++++++++++++ .../discovery/definitions/definition.json | 12 +++++++++++ 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/discovery/client.go b/discovery/client.go index 08fc8a4d6..4ba43f3c0 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -263,18 +263,6 @@ func (r *defaultClientRegistrationManager) findCredentialsAndBuildPresentation(c return nil, fmt.Errorf(errStr, service.ID, subjectDID, err) } - // add registration params as credential if not already done so by the Presentation Definition - var found bool - for _, cred := range matchingCredentials { - if cred.ID == registrationCredential.ID { - found = true - break - } - } - if !found { - matchingCredentials = append(matchingCredentials, credential.AutoCorrectSelfAttestedCredential(registrationCredential, subjectDID)) - } - return r.buildPresentation(ctx, subjectDID, service, matchingCredentials, nil) } diff --git a/discovery/client_test.go b/discovery/client_test.go index b3209a43a..4c08cc871 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -178,8 +178,7 @@ func Test_defaultClientRegistrationManager_activate(t *testing.T) { ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, _ interface{}, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { - // expect registration credential - assert.Len(t, credentials, 1) + assert.Len(t, credentials, 0) return &vpAlice, nil }) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) diff --git a/discovery/module.go b/discovery/module.go index a231d47ab..538b7d54a 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -289,26 +289,6 @@ func (m *Module) validateRegistration(definition ServiceDefinition, presentation return fmt.Errorf("verifiable presentation doesn't match required presentation definition: %w", err) } if len(creds) != len(presentation.VerifiableCredential) { - // it could be the case that the VP contains a registration credential and the matching credentials do not. - // only return errPresentationDoesNotFulfillDefinition if both contain the registration credential or neither do. - vpContainsRegistrationCredential := false - for _, cred := range presentation.VerifiableCredential { - if slices.Contains(cred.Type, credential.DiscoveryRegistrationCredentialTypeV1URI()) { - vpContainsRegistrationCredential = true - break - } - } - matchingContainsRegistrationCredential := false - for _, cred := range creds { - if slices.Contains(cred.Type, credential.DiscoveryRegistrationCredentialTypeV1URI()) { - matchingContainsRegistrationCredential = true - break - } - } - if vpContainsRegistrationCredential && !matchingContainsRegistrationCredential && len(presentation.VerifiableCredential)-len(creds) == 1 { - return nil - } - return errPresentationDoesNotFulfillDefinition } return nil diff --git a/discovery/module_test.go b/discovery/module_test.go index f0569a9fc..e04ce0ff5 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -464,6 +464,7 @@ func TestModule_Search(t *testing.T) { { Presentation: vpAlice, Fields: map[string]interface{}{ + "auth_server_url":"https://example.com/oauth2/alice", "issuer_field": authorityDID, }, Parameters: defaultRegistrationParams(aliceSubject), diff --git a/discovery/test.go b/discovery/test.go index 3c927495f..9000bb497 100644 --- a/discovery/test.go +++ b/discovery/test.go @@ -102,6 +102,16 @@ func testDefinitions() map[string]ServiceDefinition { }, }, }, + }, { + Id: "2", + Constraints: &pe.Constraints{ + Fields: []pe.Field{ + { + Id: to.Ptr("auth_server_url"), + Path: []string{"$.credentialSubject.authServerURL"}, + }, + }, + }, }, }, }, diff --git a/docs/pages/deployment/discovery.rst b/docs/pages/deployment/discovery.rst index d9365eddd..b6d1104b5 100644 --- a/docs/pages/deployment/discovery.rst +++ b/docs/pages/deployment/discovery.rst @@ -60,6 +60,7 @@ Optionally, a POST body can be provided with registration parameters, e.g.: This can be used to provide additional information. All registration parameters are returned by the search API. The ``authServerURL`` is added automatically by the Nuts node. It's constructed as ``https:///oauth2/``. +Registration parameters can only be used if the specific parameters and/or ``DiscoveryRegistrationCredential`` are required by the Presentation Definition. Once registered, future refreshes will be done automatically by the Nuts node. These refreshes could fail because of various reasons. You can check the status of the refreshes by querying the service, e.g.: @@ -156,6 +157,18 @@ Service definitions } ] } + }, { + "id": "DiscoveryRegistrationCredential", + "constraints": { + "fields": [ + { + "id": "auth_server_url", + "path": [ + "$.credentialSubject.authServerURL" + ] + } + ] + } } ] } diff --git a/e2e-tests/discovery/definitions/definition.json b/e2e-tests/discovery/definitions/definition.json index 81a01bd4f..d00b41dbf 100644 --- a/e2e-tests/discovery/definitions/definition.json +++ b/e2e-tests/discovery/definitions/definition.json @@ -46,6 +46,18 @@ } ] } + },{ + "id": "DiscoveryRegistrationCredential", + "constraints": { + "fields": [ + { + "id": "auth_server_url", + "path": [ + "$.credentialSubject.authServerURL" + ] + } + ] + } } ] } From afcebd1a083719f809ec0fbed3947858734ff3bb Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:59:27 +0200 Subject: [PATCH 45/72] Extract client ip using custom header (#3490) * add configurable header to extract client IP * add tests * rename test * make cli-docs --- README.rst | 1 + core/server_config.go | 8 +++ docs/pages/deployment/server_options.rst | 1 + http/cmd/cmd.go | 1 + http/config.go | 6 +- http/echo.go | 4 +- http/echo_test.go | 34 +++++----- http/engine.go | 40 +++++++++-- http/engine_test.go | 20 ++++++ network/network.go | 1 + network/network_integration_test.go | 1 + network/transport/grpc/config.go | 10 +++ network/transport/grpc/connection_manager.go | 2 +- network/transport/grpc/ip_interceptor.go | 66 +++++++++++++++---- network/transport/grpc/ip_interceptor_test.go | 62 ++++++++++++++++- 15 files changed, 213 insertions(+), 44 deletions(-) diff --git a/README.rst b/README.rst index 249ca7862..20748003e 100644 --- a/README.rst +++ b/README.rst @@ -197,6 +197,7 @@ The following options can be configured on the server: discovery.definitions.directory ./config/discovery Directory to load Discovery Service Definitions from. If not set, the discovery service will be disabled. If the directory contains JSON files that can't be parsed as service definition, the node will fail to start. discovery.server.ids [] IDs of the Discovery Service for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. **HTTP** + http.clientipheader X-Forwarded-For Case-sensitive HTTP Header that contains the client IP used for audit logs. For the X-Forwarded-For header only link-local, loopback, and private IPs are excluded. Switch to X-Real-IP or a custom header if you see your own proxy/infra in the logs. http.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). When debug vebosity is set the authorization headers are also logged when the request is fully logged. http.cache.maxbytes 10485760 HTTP client maximum size of the response cache in bytes. If 0, the HTTP client does not cache responses. http.internal.address 127.0.0.1:8081 Address and port the server will be listening to for internal-facing endpoints. diff --git a/core/server_config.go b/core/server_config.go index b9cfc6bfc..ac7143dd5 100644 --- a/core/server_config.go +++ b/core/server_config.go @@ -68,9 +68,17 @@ type ServerConfig struct { // LegacyTLS exists to detect usage of deprecated network.{truststorefile,certkeyfile,certfile} parameters. // This can be removed in v6.1+ (can't skip minors in migration). See https://github.com/nuts-foundation/nuts-node/issues/2909 LegacyTLS TLSConfig `koanf:"network"` + // HTTP exists to expose http.clientipheader to the nuts-network layer. + // This header should contaisn the client IP address for logging. Can be removed together with the nuts-network + HTTP HTTPConfig `koanf:"http"` configMap *koanf.Koanf } +// Config is the top-level config struct for HTTP interfaces. +type HTTPConfig struct { + ClientIPHeaderName string `koanf:"clientipheader"` +} + // HTTPClientConfig contains settings for HTTP clients. type HTTPClientConfig struct { // Timeout specifies the timeout for HTTP requests. diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 8afc2d976..e4c2c47f3 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -32,6 +32,7 @@ discovery.definitions.directory ./config/discovery Directory to load Discovery Service Definitions from. If not set, the discovery service will be disabled. If the directory contains JSON files that can't be parsed as service definition, the node will fail to start. discovery.server.ids [] IDs of the Discovery Service for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. **HTTP** + http.clientipheader X-Forwarded-For Case-sensitive HTTP Header that contains the client IP used for audit logs. For the X-Forwarded-For header only link-local, loopback, and private IPs are excluded. Switch to X-Real-IP or a custom header if you see your own proxy/infra in the logs. http.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). When debug vebosity is set the authorization headers are also logged when the request is fully logged. http.cache.maxbytes 10485760 HTTP client maximum size of the response cache in bytes. If 0, the HTTP client does not cache responses. http.internal.address 127.0.0.1:8081 Address and port the server will be listening to for internal-facing endpoints. diff --git a/http/cmd/cmd.go b/http/cmd/cmd.go index 4eedaf831..1721738eb 100644 --- a/http/cmd/cmd.go +++ b/http/cmd/cmd.go @@ -35,6 +35,7 @@ func FlagSet() *pflag.FlagSet { flags.String("http.internal.auth.audience", defs.Internal.Auth.Audience, "Expected audience for JWT tokens (default: hostname)") flags.String("http.internal.auth.authorizedkeyspath", defs.Internal.Auth.AuthorizedKeysPath, "Path to an authorized_keys file for trusted JWT signers") flags.String("http.log", string(defs.Log), fmt.Sprintf("What to log about HTTP requests. Options are '%s', '%s' (log request method, URI, IP and response code), and '%s' (log the request and response body, in addition to the metadata). When debug vebosity is set the authorization headers are also logged when the request is fully logged.", http.LogNothingLevel, http.LogMetadataLevel, http.LogMetadataAndBodyLevel)) + flags.String("http.clientipheader", defs.ClientIPHeaderName, "Case-sensitive HTTP Header that contains the client IP used for audit logs. For the X-Forwarded-For header only link-local, loopback, and private IPs are excluded. Switch to X-Real-IP or a custom header if you see your own proxy/infra in the logs.") flags.Int("http.cache.maxbytes", defs.ResponseCacheSize, "HTTP client maximum size of the response cache in bytes. If 0, the HTTP client does not cache responses.") return flags diff --git a/http/config.go b/http/config.go index 4554802df..35cd5d747 100644 --- a/http/config.go +++ b/http/config.go @@ -28,7 +28,8 @@ func DefaultConfig() Config { Public: PublicConfig{ Address: ":8080", }, - ResponseCacheSize: 10 * 1024 * 1024, // 10mb + ResponseCacheSize: 10 * 1024 * 1024, // 10mb + ClientIPHeaderName: "X-Forwarded-For", } } @@ -39,7 +40,8 @@ type Config struct { Public PublicConfig `koanf:"public"` Internal InternalConfig `koanf:"internal"` // ResponseCacheSize is the maximum number of bytes cached by HTTP clients. - ResponseCacheSize int `koanf:"cache.maxbytes"` + ResponseCacheSize int `koanf:"cache.maxbytes"` + ClientIPHeaderName string `koanf:"clientipheader"` } // PublicConfig contains the configuration for outside-facing HTTP endpoints. diff --git a/http/echo.go b/http/echo.go index dd8c07470..cda8788a9 100644 --- a/http/echo.go +++ b/http/echo.go @@ -72,7 +72,7 @@ type MultiEcho struct { // results in an error being returned. // If address wasn't used for another bind and thus leads to creating a new Echo server, it returns true. // If an existing Echo server is returned, it returns false. -func (c *MultiEcho) Bind(path string, address string, creatorFn func() (EchoServer, error)) error { +func (c *MultiEcho) Bind(path string, address string, creatorFn func(ipHeader string) (EchoServer, error), ipHeader string) error { if len(address) == 0 { return errors.New("empty address") } @@ -86,7 +86,7 @@ func (c *MultiEcho) Bind(path string, address string, creatorFn func() (EchoServ } c.binds[path] = address if _, addressBound := c.interfaces[address]; !addressBound { - server, err := creatorFn() + server, err := creatorFn(ipHeader) if err != nil { return err } diff --git a/http/echo_test.go b/http/echo_test.go index cb353773b..1dce7ff22 100644 --- a/http/echo_test.go +++ b/http/echo_test.go @@ -32,18 +32,18 @@ func Test_MultiEcho_Bind(t *testing.T) { const defaultAddress = ":1323" t.Run("group already bound", func(t *testing.T) { m := NewMultiEcho() - err := m.Bind("", defaultAddress, func() (EchoServer, error) { + err := m.Bind("", defaultAddress, func(ipHeader string) (EchoServer, error) { return echo.New(), nil - }) + }, "header") require.NoError(t, err) - err = m.Bind("", defaultAddress, func() (EchoServer, error) { + err = m.Bind("", defaultAddress, func(ipHeader string) (EchoServer, error) { return echo.New(), nil - }) + }, "header") assert.EqualError(t, err, "http bind already exists: /") }) t.Run("error - group contains subpaths", func(t *testing.T) { m := NewMultiEcho() - err := m.Bind("internal/vdr", defaultAddress, nil) + err := m.Bind("internal/vdr", defaultAddress, nil, "") assert.EqualError(t, err, "bind can't contain subpaths: internal/vdr") }) } @@ -55,9 +55,9 @@ func Test_MultiEcho_Start(t *testing.T) { server.EXPECT().Start(gomock.Any()).Return(errors.New("unable to start")) m := NewMultiEcho() - m.Bind("group2", ":8080", func() (EchoServer, error) { + m.Bind("group2", ":8080", func(ipHeader string) (EchoServer, error) { return server, nil - }) + }, "header") err := m.Start() assert.EqualError(t, err, "unable to start") }) @@ -84,22 +84,22 @@ func Test_MultiEcho(t *testing.T) { // Bind interfaces m := NewMultiEcho() - err := m.Bind(RootPath, defaultAddress, func() (EchoServer, error) { + err := m.Bind(RootPath, defaultAddress, func(ipHeader string) (EchoServer, error) { return defaultServer, nil - }) + }, "header") require.NoError(t, err) - err = m.Bind("internal", "internal:8080", func() (EchoServer, error) { + err = m.Bind("internal", "internal:8080", func(ipHeader string) (EchoServer, error) { return internalServer, nil - }) + }, "header") require.NoError(t, err) - err = m.Bind("public", "public:8080", func() (EchoServer, error) { + err = m.Bind("public", "public:8080", func(ipHeader string) (EchoServer, error) { return publicServer, nil - }) + }, "header") require.NoError(t, err) - err = m.Bind("extra-public", "public:8080", func() (EchoServer, error) { + err = m.Bind("extra-public", "public:8080", func(ipHeader string) (EchoServer, error) { t.Fatal("should not be called!") return nil, nil - }) + }, "header") require.NoError(t, err) m.addFn(http.MethodPost, "/public/pub-endpoint", nil) @@ -129,9 +129,9 @@ func Test_MultiEcho_Methods(t *testing.T) { ) m := NewMultiEcho() - m.Bind(RootPath, ":1323", func() (EchoServer, error) { + m.Bind(RootPath, ":1323", func(ipHeader string) (EchoServer, error) { return defaultServer, nil - }) + }, "header") m.GET("/get", nil) m.POST("/post", nil) m.PUT("/put", nil) diff --git a/http/engine.go b/http/engine.go index d64941bbe..fa3c1f673 100644 --- a/http/engine.go +++ b/http/engine.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "github.com/nuts-foundation/nuts-node/http/client" + "net" "net/http" "os" "strings" @@ -79,12 +80,12 @@ func (h *Engine) Configure(serverConfig core.ServerConfig) error { h.server = NewMultiEcho() // Public endpoints - if err := h.server.Bind(RootPath, h.config.Public.Address, h.createEchoServer); err != nil { + if err := h.server.Bind(RootPath, h.config.Public.Address, h.createEchoServer, h.config.ClientIPHeaderName); err != nil { return err } // Internal endpoints for _, httpPath := range []string{"/internal", "/status", "/health", "/metrics"} { - if err := h.server.Bind(httpPath, h.config.Internal.Address, h.createEchoServer); err != nil { + if err := h.server.Bind(httpPath, h.config.Internal.Address, h.createEchoServer, h.config.ClientIPHeaderName); err != nil { return err } } @@ -102,7 +103,7 @@ func (h *Engine) configureClient(serverConfig core.ServerConfig) { } } -func (h *Engine) createEchoServer() (EchoServer, error) { +func (h *Engine) createEchoServer(ipHeader string) (EchoServer, error) { echoServer := echo.New() echoServer.HideBanner = true echoServer.HidePort = true @@ -110,8 +111,15 @@ func (h *Engine) createEchoServer() (EchoServer, error) { // ErrorHandler echoServer.HTTPErrorHandler = core.CreateHTTPErrorHandler() - // Reverse proxies must set the X-Forwarded-For header to the original client IP. - echoServer.IPExtractor = echo.ExtractIPFromXFFHeader() + // Extract original client IP from configured header. + switch ipHeader { + case echo.HeaderXForwardedFor: + echoServer.IPExtractor = echo.ExtractIPFromXFFHeader() + case "": + echoServer.IPExtractor = echo.ExtractIPDirect() // sensible fallback; use source address from IPv4/IPv6 packet header if there is no HTTP header. + default: + echoServer.IPExtractor = extractIPFromCustomHeader(ipHeader) + } return &echoAdapter{ startFn: echoServer.Start, @@ -253,3 +261,25 @@ func (h Engine) applyAuthMiddleware(echoServer core.EchoRouter, path string, con return nil } + +// extractIPFromCustomHeader extracts an IP address from any custom header. +// If the header is missing or contains an invalid IP, the extractor returns the ip from the request. +// This is an altered version of echo.ExtractIPFromRealIPHeader() that does not check for trusted IPs. +func extractIPFromCustomHeader(ipHeader string) echo.IPExtractor { + extractIP := echo.ExtractIPDirect() + return func(req *http.Request) string { + directIP := extractIP(req) // source address from IPv4/IPv6 packet header + realIP := req.Header.Get(ipHeader) + if realIP == "" { + return directIP + } + + realIP = strings.TrimPrefix(realIP, "[") + realIP = strings.TrimSuffix(realIP, "]") + if rIP := net.ParseIP(realIP); rIP != nil { + return realIP + } + + return directIP + } +} diff --git a/http/engine_test.go b/http/engine_test.go index f0e38b236..31d36d5e1 100644 --- a/http/engine_test.go +++ b/http/engine_test.go @@ -131,6 +131,7 @@ func TestEngine_Configure(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "http://"+engine.config.Internal.Address+securedPath, nil) log.Logger().Infof("requesting %v", request.URL.String()) request.Header.Set("Authorization", "Bearer "+string(serializedToken)) + request.Header.Set(engine.config.ClientIPHeaderName, "1.2.3.4") response, err := http.DefaultClient.Do(request) assert.NoError(t, err) @@ -254,6 +255,7 @@ func TestEngine_LoggingMiddleware(t *testing.T) { t.Run("requestLogger", func(t *testing.T) { engine := New(noop, nil) engine.config.Internal.Address = fmt.Sprintf("localhost:%d", test.FreeTCPPort()) + engine.config.ClientIPHeaderName = "X-Custom-Header" err := engine.Configure(*core.NewServerConfig()) require.NoError(t, err) @@ -283,6 +285,24 @@ func TestEngine_LoggingMiddleware(t *testing.T) { _, _ = http.Get("http://" + engine.config.Internal.Address + "/some-path") assert.Contains(t, output.String(), "HTTP request") }) + t.Run("ip log - custom header", func(t *testing.T) { + // Call to another, registered path is logged + output.Reset() + request, _ := http.NewRequest(http.MethodGet, "http://"+engine.config.Internal.Address+"/some-path", nil) + request.Header.Set(engine.config.ClientIPHeaderName, "1.2.3.4") + _, err = http.DefaultClient.Do(request) + require.NoError(t, err) + assert.Contains(t, output.String(), "remote_ip=1.2.3.4") + }) + t.Run("ip log - custom header missing", func(t *testing.T) { + // Call to another, registered path is logged + output.Reset() + request, _ := http.NewRequest(http.MethodGet, "http://"+engine.config.Internal.Address+"/some-path", nil) + request.Header.Set(engine.config.ClientIPHeaderName, "") + _, err = http.DefaultClient.Do(request) + require.NoError(t, err) + assert.Contains(t, output.String(), "remote_ip=127.0.0.1") + }) }) t.Run("bodyLogger", func(t *testing.T) { engine := New(noop, nil) diff --git a/network/network.go b/network/network.go index 0f148d2fe..be8618bdd 100644 --- a/network/network.go +++ b/network/network.go @@ -295,6 +295,7 @@ func (n *Network) Configure(config core.ServerConfig) error { return fmt.Errorf("failed to open connections store: %w", err) } + grpcOpts = append(grpcOpts, grpc.WithClientIPHeader(config.HTTP.ClientIPHeaderName)) connectionManCfg, err := grpc.NewConfig(n.config.GrpcAddr, n.peerID, grpcOpts...) if err != nil { return err diff --git a/network/network_integration_test.go b/network/network_integration_test.go index 593fe6d53..e1d18e557 100644 --- a/network/network_integration_test.go +++ b/network/network_integration_test.go @@ -901,6 +901,7 @@ func TestNetworkIntegration_TLSOffloading(t *testing.T) { node1 := startNode(t, "node1", testDirectory, func(serverCfg *core.ServerConfig, cfg *Config) { serverCfg.TLS.Offload = core.OffloadIncomingTLS serverCfg.TLS.ClientCertHeaderName = "client-cert" + serverCfg.HTTP.ClientIPHeaderName = "X-Forwarded-For" }) // Create client (node2) that connects to server node diff --git a/network/transport/grpc/config.go b/network/transport/grpc/config.go index c39aa4f79..9940e37bd 100644 --- a/network/transport/grpc/config.go +++ b/network/transport/grpc/config.go @@ -76,6 +76,14 @@ func WithTLS(clientCertificate tls.Certificate, trustStore *core.TrustStore, pki } } +// WithClientIPHeader sets the HTTP header that is used to extract the client IP. +func WithClientIPHeader(clientIPHeaderName string) ConfigOption { + return func(config *Config) error { + config.clientIPHeaderName = clientIPHeaderName + return nil + } +} + // WithTLSOffloading enables TLS for outgoing connections, but is offloaded for incoming connections. // It MUST be used in conjunction, but after with WithTLS. func WithTLSOffloading(clientCertHeaderName string) ConfigOption { @@ -121,6 +129,8 @@ type Config struct { pkiValidator pki.Validator // clientCertHeaderName specifies the name of the HTTP header that contains the client certificate, if TLS is offloaded. clientCertHeaderName string + // clientIPHeaderName specifies the name of the HTTP header that contains the client IP address. + clientIPHeaderName string // connectionTimeout specifies the time before an outbound connection attempt times out. connectionTimeout time.Duration // listener holds a function to create the net.Listener that is used for inbound connections. diff --git a/network/transport/grpc/connection_manager.go b/network/transport/grpc/connection_manager.go index 12371dd02..f738a56fe 100644 --- a/network/transport/grpc/connection_manager.go +++ b/network/transport/grpc/connection_manager.go @@ -185,7 +185,7 @@ func newGrpcServer(config Config) (*grpc.Server, error) { } // Chain interceptors. ipInterceptor is added last, so it processes the stream first. - serverInterceptors = append(serverInterceptors, ipInterceptor) + serverInterceptors = append(serverInterceptors, ipInterceptor(config.clientIPHeaderName)) serverOpts = append(serverOpts, grpc.ChainStreamInterceptor(serverInterceptors...)) // Create gRPC server for inbound connectionList and associate it with the protocols diff --git a/network/transport/grpc/ip_interceptor.go b/network/transport/grpc/ip_interceptor.go index 14f0e1115..992d8c1f1 100644 --- a/network/transport/grpc/ip_interceptor.go +++ b/network/transport/grpc/ip_interceptor.go @@ -32,22 +32,35 @@ const headerXForwardedFor = "X-Forwarded-For" // ipInterceptor tries to extract the IP from the X-Forwarded-For header and sets this as the peers address. // No address is set if the header is unavailable. -func ipInterceptor(srv interface{}, serverStream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - addr := addrFrom(extractIPFromXFFHeader(serverStream)) - if addr == nil { - // Exit without change if there is no X-Forwarded-For in the http header, - // or if no IP could be extracted from the header. - // This will default to the IP found in lvl 4 header. - return handler(srv, serverStream) +func ipInterceptor(ipHeader string) grpc.StreamServerInterceptor { + var extractIPHeader func(serverStream grpc.ServerStream) string + switch ipHeader { + case headerXForwardedFor: + extractIPHeader = extractIPFromXFFHeader + case "": + extractIPHeader = func(_ grpc.ServerStream) string { + return "" + } + default: + extractIPHeader = extractIPFromCustomHeader(ipHeader) } + return func(srv interface{}, serverStream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + addr := addrFrom(extractIPHeader(serverStream)) + if addr == nil { + // Exit without change if there is no X-Forwarded-For in the http header, + // or if no IP could be extracted from the header. + // This will default to the IP found in lvl 4 header. + return handler(srv, serverStream) + } - peerInfo, _ := peer.FromContext(serverStream.Context()) - if peerInfo == nil { - peerInfo = &peer.Peer{} + peerInfo, _ := peer.FromContext(serverStream.Context()) + if peerInfo == nil { + peerInfo = &peer.Peer{} + } + peerInfo.Addr = addr + ctx := peer.NewContext(serverStream.Context(), peerInfo) + return handler(srv, &wrappedServerStream{ctx: ctx, ServerStream: serverStream}) } - peerInfo.Addr = addr - ctx := peer.NewContext(serverStream.Context(), peerInfo) - return handler(srv, &wrappedServerStream{ctx: ctx, ServerStream: serverStream}) } // extractIPFromXFFHeader tries to retrieve the address from X-Forward-For header. Returns an empty string if non found. @@ -64,7 +77,7 @@ func extractIPFromXFFHeader(serverStream grpc.ServerStream) string { } ips := strings.Split(strings.Join(xffs, ","), ",") for i := len(ips) - 1; i >= 0; i-- { - ip := net.ParseIP(strings.TrimSpace(ips[i])) + ip := net.ParseIP(trimIP(ips[i])) if ip == nil { // Unable to parse IP; cannot trust entire records return ipUnknown @@ -97,3 +110,28 @@ func addrFrom(ip string) net.Addr { func isInternal(ip net.IP) bool { return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() } + +// extractIPFromCustomHeader extracts an IP address from any custom header. +// This is an altered version of echo.ExtractIPFromRealIPHeader() that does not check for trusted IPs. +func extractIPFromCustomHeader(ipHeader string) func(serverStream grpc.ServerStream) string { + return func(serverStream grpc.ServerStream) string { + ipUnknown := "" + md, ok := metadata.FromIncomingContext(serverStream.Context()) + if !ok { + return ipUnknown + } + header := md.Get(ipHeader) + if len(header) != 1 { + return ipUnknown + } + return trimIP(header[0]) + } +} + +func trimIP(ip string) string { + ip = strings.TrimSpace(ip) + // trim brackets from IPv6 + ip = strings.TrimPrefix(ip, "[") + ip = strings.TrimSuffix(ip, "]") + return ip +} diff --git a/network/transport/grpc/ip_interceptor_test.go b/network/transport/grpc/ip_interceptor_test.go index a16a3ca21..8e6f4a36c 100644 --- a/network/transport/grpc/ip_interceptor_test.go +++ b/network/transport/grpc/ip_interceptor_test.go @@ -61,6 +61,7 @@ func Test_ipInterceptor(t *testing.T) { IP: ipAddr.AsSlice(), Zone: ipAddr.Zone(), } + ipv6 = "[" + ipv6 + "]" // trims brackets from ipv6 internalXFFAddr, _ := net.ResolveIPAddr("ip", internalIPs[0]) peerNoAddres := net.Addr(nil) @@ -79,12 +80,15 @@ func Test_ipInterceptor(t *testing.T) { } for _, tc := range tests { + ran := false serverStream.ctx = contextWithMD(strings.Join(tc.xffIPs, ",")) - _ = ipInterceptor(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { + _ = ipInterceptor(headerXForwardedFor)(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { peerInfo, success = peer.FromContext(wrappedStream.Context()) + ran = true return nil }) + require.True(t, ran, "test logic was not executed") if success { assert.Equal(t, tc.expected.String(), peerInfo.Addr.String()) } else { @@ -93,21 +97,73 @@ func Test_ipInterceptor(t *testing.T) { } }) t.Run("no XXF header", func(t *testing.T) { + ran := false serverStream.ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{})) - _ = ipInterceptor(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { + _ = ipInterceptor(headerXForwardedFor)(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { peerInfo, success = peer.FromContext(wrappedStream.Context()) + ran = true return nil }) + require.True(t, ran, "test logic was not executed") assert.False(t, success) assert.Nil(t, peerInfo) }) t.Run("no metadata in context", func(t *testing.T) { + ran := false serverStream.ctx = context.Background() - _ = ipInterceptor(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { + _ = ipInterceptor(headerXForwardedFor)(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { peerInfo, success = peer.FromContext(wrappedStream.Context()) + ran = true return nil }) + require.True(t, ran, "test logic was not executed") assert.False(t, success) assert.Nil(t, peerInfo) }) + t.Run("custom header", func(t *testing.T) { + header := "X-Custom-Header" + expectedIP := "1.2.3.4" + t.Run("ok", func(t *testing.T) { + ran := false + md := metadata.New(map[string]string{header: expectedIP}) + serverStream.ctx = metadata.NewIncomingContext(context.Background(), md) + _ = ipInterceptor(header)(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { + peerInfo, success = peer.FromContext(wrappedStream.Context()) + ran = true + return nil + }) + + require.True(t, ran, "test logic was not executed") + assert.True(t, success) + assert.Equal(t, expectedIP, peerInfo.Addr.String()) + }) + t.Run("empty header", func(t *testing.T) { + ran := false + md := metadata.New(map[string]string{header: ""}) + serverStream.ctx = metadata.NewIncomingContext(context.Background(), md) + _ = ipInterceptor(header)(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { + peerInfo, success = peer.FromContext(wrappedStream.Context()) + ran = true + return nil + }) + + require.True(t, ran, "test logic was not executed") + assert.False(t, success) + assert.Nil(t, peerInfo) + }) + t.Run("invalid input on custom header", func(t *testing.T) { + ran := false + md := metadata.New(map[string]string{header: strings.Join([]string{expectedIP, expectedIP}, ",")}) + serverStream.ctx = metadata.NewIncomingContext(context.Background(), md) + _ = ipInterceptor(header)(nil, serverStream, nil, func(srv interface{}, wrappedStream grpc.ServerStream) error { + peerInfo, success = peer.FromContext(wrappedStream.Context()) + ran = true + return nil + }) + + require.True(t, ran, "test logic was not executed") + assert.False(t, success) + assert.Nil(t, peerInfo) + }) + }) } From c8adb2c69016ed2f6a901c59fc947a1034884435 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:07:46 +0200 Subject: [PATCH 46/72] update go-did to v0.15.0 (#3492) --- go.mod | 2 +- go.sum | 4 ++-- vcr/verifier/signature_verifier.go | 3 +++ vcr/verifier/signature_verifier_test.go | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index eb5442895..7b64dd1f0 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/nats-io/nats-server/v2 v2.10.21 github.com/nats-io/nats.go v1.37.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b - github.com/nuts-foundation/go-did v0.14.0 + github.com/nuts-foundation/go-did v0.15.0 github.com/nuts-foundation/go-leia/v4 v4.0.3 github.com/nuts-foundation/go-stoabs v1.10.0 github.com/nuts-foundation/sqlite v1.0.0 diff --git a/go.sum b/go.sum index 9b9b5e51e..e5d5c5fa7 100644 --- a/go.sum +++ b/go.sum @@ -350,8 +350,8 @@ github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAm github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b h1:80icUxWHwE1MrIOOEK5rxrtyKOgZeq5Iu1IjAEkggTY= github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b/go.mod h1:6YUioYirD6/8IahZkoS4Ypc8xbeJW76Xdk1QKcziNTM= -github.com/nuts-foundation/go-did v0.14.0 h1:Y1tuQCC2xmDX1bdXQS9iquwzJgcT1zcJxbZkqC5Dfac= -github.com/nuts-foundation/go-did v0.14.0/go.mod h1:dQm9b2dYUnhgVW1FmpAi5nNe0mfIrnxM3EaQx4GsDhI= +github.com/nuts-foundation/go-did v0.15.0 h1:aNl6KC8jiyRJGl9PPKFBboLLC0wUm5h+tjE1UBDQEPw= +github.com/nuts-foundation/go-did v0.15.0/go.mod h1:swjCJvcRxc+i1nyieIERWEb3vFb4N7iYC+qen2OIbNg= github.com/nuts-foundation/go-leia/v4 v4.0.3 h1:xNZznXWvcIwonXIDmpDDvF7KmP9BOK0MFt9ir3RD2gI= github.com/nuts-foundation/go-leia/v4 v4.0.3/go.mod h1:tYveGED8tSbQYhZNv2DVTc51c2zEWmSF+MG96PAtalY= github.com/nuts-foundation/go-stoabs v1.10.0 h1:mNzm9jgraMc69a8gTgteli8t1CMxr1+gyI7A9Eh0NDk= diff --git a/vcr/verifier/signature_verifier.go b/vcr/verifier/signature_verifier.go index 8e498c7f5..824bed7fd 100644 --- a/vcr/verifier/signature_verifier.go +++ b/vcr/verifier/signature_verifier.go @@ -84,6 +84,9 @@ func (sv *signatureVerifier) jsonldProof(documentToVerify any, issuer string, at // for a VP this will not fail verificationMethod := ldProof.VerificationMethod.String() + if verificationMethod == "" { + return newVerificationError("missing proof") + } verificationMethodIssuer := strings.Split(verificationMethod, "#")[0] if verificationMethodIssuer == "" || verificationMethodIssuer != issuer { return errVerificationMethodNotOfIssuer diff --git a/vcr/verifier/signature_verifier_test.go b/vcr/verifier/signature_verifier_test.go index fdfa327ec..cd1e9169e 100644 --- a/vcr/verifier/signature_verifier_test.go +++ b/vcr/verifier/signature_verifier_test.go @@ -219,7 +219,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { err := sv.VerifySignature(vc2, nil) - assert.EqualError(t, err, "verification error: unsupported proof type: json: cannot unmarshal array into Go value of type proof.LDProof") + assert.EqualError(t, err, "verification error: missing proof") }) t.Run("error - wrong jws in proof", func(t *testing.T) { From e9c8506cb7ac831db96f7f8c62de6478cd8aaa18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 08:10:07 +0200 Subject: [PATCH 47/72] Bump github.com/redis/go-redis/v9 from 9.6.2 to 9.7.0 (#3500) Bumps [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) from 9.6.2 to 9.7.0. - [Release notes](https://github.com/redis/go-redis/releases) - [Changelog](https://github.com/redis/go-redis/blob/master/CHANGELOG.md) - [Commits](https://github.com/redis/go-redis/compare/v9.6.2...v9.7.0) --- updated-dependencies: - dependency-name: github.com/redis/go-redis/v9 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7b64dd1f0..5fd3bcbcf 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/privacybydesign/irmago v0.16.0 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.0 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index e5d5c5fa7..856c33ee7 100644 --- a/go.sum +++ b/go.sum @@ -396,8 +396,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk= -github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= From b1e0119dbbe0c3124a6ca438144f732d5c0ae572 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 18 Oct 2024 08:14:22 +0200 Subject: [PATCH 48/72] fix tokenIntrospection response description (#3495) --- docs/_static/auth/v2.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/_static/auth/v2.yaml b/docs/_static/auth/v2.yaml index a31302e74..e6bd2aa5f 100644 --- a/docs/_static/auth/v2.yaml +++ b/docs/_static/auth/v2.yaml @@ -592,16 +592,16 @@ components: $ref: '#/components/schemas/cnf' iss: type: string - description: Contains the DID of the authorizer. Should be equal to 'sub' - example: did:web:example.com:resource-owner + description: Issuer URL of the authorizer. + example: https://example.com/oauth2/authorizer aud: type: string description: RFC7662 - Service-specific string identifier or list of string identifiers representing the intended audience for this token, as defined in JWT [RFC7519]. example: "https://target_token_endpoint" client_id: type: string - description: The client (DID) the access token was issued to - example: did:web:example.com:client + description: The client identity the access token was issued to. Since the Verifiable Presentation is used to grant access, the client_id reflects the client_id in the access token request. + example: https://example.com/oauth2/client exp: type: integer description: Expiration date in seconds since UNIX epoch From 7d8da8cfdd5d0521d0cce51174f6ea48d9b5bf0d Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:01:29 +0200 Subject: [PATCH 49/72] Add statuslist revocation to e2e-test and fix failing code (#3497) --- e2e-tests/oauth-flow/rfc021/do-test.sh | 29 +++++++++++++++++++++- vcr/revocation/bitstring.go | 11 ++++++--- vcr/revocation/statuslist2021_issuer.go | 33 +++++++++++-------------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/e2e-tests/oauth-flow/rfc021/do-test.sh b/e2e-tests/oauth-flow/rfc021/do-test.sh index a91234153..b6b42ab03 100755 --- a/e2e-tests/oauth-flow/rfc021/do-test.sh +++ b/e2e-tests/oauth-flow/rfc021/do-test.sh @@ -71,7 +71,7 @@ echo "Status list credential: $STATUS_LIST_CREDENTIAL" # Get status list credential RESPONSE=$($db_dc exec nodeB-backend curl -s -k $STATUS_LIST_CREDENTIAL) # Check response HTTP 200 OK -if [ $? -eq 0 ]; then +if echo $RESPONSE | grep -q "\"id\":\"$STATUS_LIST_CREDENTIAL\"" ; then echo "Status list credential retrieved" else echo "FAILED: Could not retrieve status list credential" 1>&2 @@ -195,6 +195,33 @@ else exitWithDockerLogs 1 fi +echo "------------------------------------" +echo "Revoking credential..." +echo "------------------------------------" +# revoke credential +VENDOR_B_CREDENTIAL_ID=$(echo $VENDOR_B_CREDENTIAL | jq -r .id) +echo $VENDOR_B_CREDENTIAL_ID +RESPONSE=$(curl -s -X DELETE "http://localhost:28081/internal/vcr/v2/issuer/vc/${VENDOR_B_CREDENTIAL_ID//#/%23}") +if [ -z "${RESPONSE}" ]; then + echo "VendorB NutsOrganizationCredential revoked" +else + echo "FAILED: NutsOrganizationCredential not revoked" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + +echo "------------------------------------" +echo "Retrieving data fails..." +echo "------------------------------------" +RESPONSE=$($db_dc exec nodeB curl --http1.1 --insecure --cert /etc/nginx/ssl/server.pem --key /etc/nginx/ssl/key.pem https://nodeA:443/resource -H "Authorization: DPoP $ACCESS_TOKEN" -H "DPoP: $DPOP") +if [ "${RESPONSE}" == "Unauthorized" ]; then + echo "Access denied!" +else + echo "FAILED: Retrieved data with revoked credential" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + echo "------------------------------------" echo "Stopping Docker containers..." echo "------------------------------------" diff --git a/vcr/revocation/bitstring.go b/vcr/revocation/bitstring.go index 9529876f7..952a897c2 100644 --- a/vcr/revocation/bitstring.go +++ b/vcr/revocation/bitstring.go @@ -54,9 +54,14 @@ func (bs *bitstring) Scan(value any) error { *bs = nil return nil } - asString, ok := value.(string) - if !ok { - return fmt.Errorf("bitstring unmarshal from DB: expected []uint8, got %T", value) + var asString string + switch v := value.(type) { + case string: // sqlite, postgress, sqlserver + asString = v + case []uint8: // mysql + asString = string(v) + default: + return fmt.Errorf("bitstring unmarshal from DB: expected []uint8 or string, got %T", value) } expanded, err := expand(asString) if err != nil { diff --git a/vcr/revocation/statuslist2021_issuer.go b/vcr/revocation/statuslist2021_issuer.go index 74d482c8c..6588deecd 100644 --- a/vcr/revocation/statuslist2021_issuer.go +++ b/vcr/revocation/statuslist2021_issuer.go @@ -166,18 +166,7 @@ func (cs *StatusList2021) Credential(ctx context.Context, issuerDID did.DID, pag var cred *vc.VerifiableCredential // is nil, so if this panics outside this method the var name is probably shadowed in the db.Transaction. err = cs.db.Transaction(func(tx *gorm.DB) error { // lock credentialRecord row for statusListCredentialURL since it will be updated. - // Revoke does the same to guarantee the DB always contains all revocations. - // Microsoft SQL server does not support the locking clause, so we have to use a raw query instead. - // See https://github.com/nuts-foundation/nuts-node/issues/3393 - if tx.Dialector.Name() == "sqlserver" { - err = tx.Raw("SELECT * FROM status_list_credential WITH (UPDLOCK, ROWLOCK) WHERE subject_id = ?", statusListCredentialURL). - Scan(new(credentialRecord)). - Error - } else { - err = tx.Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate}). - Find(new(credentialRecord), "subject_id = ?", statusListCredentialURL). - Error - } + err = lockCredentialRecord(tx, statusListCredentialURL) if err != nil { return err } @@ -421,12 +410,7 @@ func (cs *StatusList2021) Revoke(ctx context.Context, credentialID ssi.URI, entr return cs.db.Transaction(func(tx *gorm.DB) error { // lock relevant credentialRecord. It was created when the first entry was issued for this StatusList2021Credential. - err = tx.Model(new(credentialRecord)). - Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate}). - Select("subject_id"). - Where("subject_id = ?", entry.StatusListCredential). - Find(new([]string)). - Error + err = lockCredentialRecord(tx, entry.StatusListCredential) if err != nil { return err } @@ -478,3 +462,16 @@ func (cs *StatusList2021) statusListURL(issuer did.DID, page int) string { result, _ := url.Parse(cs.baseURL) return result.JoinPath("statuslist", issuer.String(), strconv.Itoa(page)).String() } + +func lockCredentialRecord(tx *gorm.DB, statusListCredentialURL string) error { + // Microsoft SQL server does not support the locking clause, so we have to use a raw query instead. + // See https://github.com/nuts-foundation/nuts-node/issues/3393 + if tx.Dialector.Name() == "sqlserver" { + return tx.Raw("SELECT * FROM status_list_credential WITH (UPDLOCK, ROWLOCK) WHERE subject_id = ?", statusListCredentialURL). + Scan(new(credentialRecord)). + Error + } + return tx.Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate}). + Find(new(credentialRecord), "subject_id = ?", statusListCredentialURL). + Error +} From de4f587ffffd0b0f012f1afd48c435b82c355e6e Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:01:44 +0200 Subject: [PATCH 50/72] Discovery service retraction fix (#3498) * broken test * fix retraction * move retraction type from .type to .vp.type --- discovery/client.go | 16 ++++++----- discovery/client_test.go | 6 ++++- discovery/module.go | 8 ++++-- e2e-tests/discovery/run-test.sh | 48 ++++++++++++++++++++++++++++++--- 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/discovery/client.go b/discovery/client.go index 4ba43f3c0..059181c44 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -227,7 +227,7 @@ func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, servi func (r *defaultClientRegistrationManager) deregisterPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, vp vc.VerifiablePresentation) error { presentation, err := r.buildPresentation(ctx, subjectDID, service, nil, map[string]interface{}{ "retract_jti": vp.ID.String(), - }) + }, &retractionPresentationType) if err != nil { return err } @@ -263,15 +263,18 @@ func (r *defaultClientRegistrationManager) findCredentialsAndBuildPresentation(c return nil, fmt.Errorf(errStr, service.ID, subjectDID, err) } - return r.buildPresentation(ctx, subjectDID, service, matchingCredentials, nil) + return r.buildPresentation(ctx, subjectDID, service, matchingCredentials, nil, nil) } -func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, - credentials []vc.VerifiableCredential, additionalProperties map[string]interface{}) (*vc.VerifiablePresentation, error) { +func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, credentials []vc.VerifiableCredential, additionalProperties map[string]interface{}, additionalVPType *ssi.URI) (*vc.VerifiablePresentation, error) { nonce := nutsCrypto.GenerateNonce() // Make sure the presentation is not valid for longer than the max validity as defined by the Service Definitio. expires := time.Now().Add(time.Duration(service.PresentationMaxValidity-1) * time.Second).Truncate(time.Second) holderURI := subjectDID.URI() + var additionalVPTypes []ssi.URI + if additionalVPType != nil { + additionalVPTypes = append(additionalVPTypes, *additionalVPType) + } return r.vcr.Wallet().BuildPresentation(ctx, credentials, holder.PresentationOptions{ ProofOptions: proof.ProofOptions{ Created: time.Now(), @@ -280,8 +283,9 @@ func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context Nonce: &nonce, AdditionalProperties: additionalProperties, }, - Format: vc.JWTPresentationProofFormat, - Holder: &holderURI, + AdditionalTypes: additionalVPTypes, + Format: vc.JWTPresentationProofFormat, + Holder: &holderURI, }, &subjectDID, false) } diff --git a/discovery/client_test.go b/discovery/client_test.go index 4c08cc871..928722e41 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -220,7 +220,11 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { t.Run("registered", func(t *testing.T) { ctx := newTestContext(t) ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()) - ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn( + func(ctx context.Context, credentials []vc.VerifiableCredential, options holder.PresentationOptions, signerDID *did.DID, validateVC bool) (*vc.VerifiablePresentation, error) { + assert.Equal(t, options.AdditionalTypes[0], retractionPresentationType) + return &vpAlice, nil // not a revocation VP + }) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) _, err := ctx.store.add(testServiceID, vpAlice, testSeed, 1) require.NoError(t, err) diff --git a/discovery/module.go b/discovery/module.go index 538b7d54a..86d5ef678 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -295,11 +295,15 @@ func (m *Module) validateRegistration(definition ServiceDefinition, presentation } func (m *Module) validateRetraction(serviceID string, presentation vc.VerifiablePresentation) error { - // Presentation might be a retraction (deletion of an earlier credentialRecord) must contain no credentials, and refer to the VP being retracted by ID. - // If those conditions aren't met, we don't need to register the retraction. + // RFC022 §3.4:it MUST specify RetractedVerifiablePresentation as type, in addition to the VerifiablePresentation. + // presentation.IsType(retractionPresentationType) // satisfied by the switch one level up + + // RFC022 §3.4: it MUST NOT contain any credentials. if len(presentation.VerifiableCredential) > 0 { return errRetractionContainsCredentials } + + // RFC022 §3.4: it MUST contain a retract_jti JWT claim, containing the jti of the presentation to retract. // Check that the retraction refers to an existing presentation. // If not, it might've already been removed due to expiry or superseded by a newer presentation. retractJTIRaw, _ := presentation.JWT().Get("retract_jti") diff --git a/e2e-tests/discovery/run-test.sh b/e2e-tests/discovery/run-test.sh index dd88659cf..5c4cd38d2 100755 --- a/e2e-tests/discovery/run-test.sh +++ b/e2e-tests/discovery/run-test.sh @@ -48,7 +48,13 @@ fi echo "---------------------------------------" echo "Registering care organization on Discovery Service..." echo "---------------------------------------" -curl --insecure -s -X POST http://localhost:28081/internal/discovery/v1/dev:eOverdracht2023/${SUBJECT} +RESPONSE=$(curl --insecure -s -X POST http://localhost:28081/internal/discovery/v1/dev:eOverdracht2023/${SUBJECT}) +if [ -z "${RESPONSE}" ]; then + echo "Registered for service" +else + echo "FAILED: Could not register for Discovery Service" 1>&2 + exitWithDockerLogs 1 +fi # Registration refresh interval is 500ms, wait some to make sure the registration is refreshed sleep 2 @@ -68,8 +74,6 @@ fi echo "---------------------------------------" echo "Searching for care organization registration on Discovery Client..." echo "---------------------------------------" -# Service refresh interval is 500ms, wait some to make sure the presentations are loaded -sleep 2 RESPONSE=$(curl -s --insecure "http://localhost:28081/internal/discovery/v1/dev:eOverdracht2023?credentialSubject.organization.name=Care*") NUM_ITEMS=$(echo $RESPONSE | jq length) if [ $NUM_ITEMS -eq 1 ]; then @@ -87,6 +91,44 @@ else exitWithDockerLogs 1 fi +echo "---------------------------------------" +echo "Retract Discovery Service registration..." +echo "---------------------------------------" +RESPONSE=$(curl --insecure -s -X DELETE http://localhost:28081/internal/discovery/v1/dev:eOverdracht2023/${SUBJECT}) +if [ -z "${RESPONSE}" ]; then + echo "Registration revoked" +else + echo "FAILED: Registration not (immediately) revoked" 1>&2 + exitWithDockerLogs 1 +fi + +# Registration refresh interval is 500ms, wait some to make sure the registration is refreshed +sleep 2 + +echo "---------------------------------------" +echo "Searching for care organization registration on Discovery Server..." +echo "---------------------------------------" +RESPONSE=$(curl -s --insecure "http://localhost:18081/internal/discovery/v1/dev:eOverdracht2023?credentialSubject.organization.name=Care*") +NUM_ITEMS=$(echo $RESPONSE | jq length) +if [ $NUM_ITEMS -eq 0 ]; then + echo "Registration not found" +else + echo "FAILED: Found registration" 1>&2 + exitWithDockerLogs 1 +fi + +echo "---------------------------------------" +echo "Searching for care organization registration on Discovery Client..." +echo "---------------------------------------" +RESPONSE=$(curl -s --insecure "http://localhost:28081/internal/discovery/v1/dev:eOverdracht2023?credentialSubject.organization.name=Care*") +NUM_ITEMS=$(echo $RESPONSE | jq length) +if [ $NUM_ITEMS -eq 0 ]; then + echo "Registration not found" +else + echo "FAILED: Found registration" 1>&2 + exitWithDockerLogs 1 +fi + echo "------------------------------------" echo "Stopping Docker containers..." echo "------------------------------------" From 4e53223d07b17d496eba81cb74c79ef91c13489c Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:02:37 +0200 Subject: [PATCH 51/72] always add kid to header (#3499) --- crypto/jwx.go | 13 +++++++------ crypto/jwx_test.go | 18 ++++++++++++++++++ crypto/memory.go | 4 +--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crypto/jwx.go b/crypto/jwx.go index a0779d15e..381f42a2a 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -82,9 +82,7 @@ func (client *Crypto) SignJWS(ctx context.Context, payload []byte, headers map[s return "", err } - if _, ok := headers["jwk"]; !ok { - headers["kid"] = kid - } + headers["kid"] = kid return SignJWS(ctx, payload, headers, privateKey, detached) } @@ -247,10 +245,13 @@ func SignJWS(ctx context.Context, payload []byte, protectedHeaders map[string]in return "", fmt.Errorf("unable to set header %s: %w", key, err) } } - // The JWX library is fine with creating a JWK for a private key (including the private exponents), so - // we want to make sure the `jwk` header (if present) does not (accidentally) contain a private key. - // That would lead to the node leaking its private key material in the resulting JWS which would be very, very bad. if headers.JWK() != nil { + // 'kid' has been logged, use 'jwk' to sign + _ = headers.Remove(jwk.KeyIDKey) + + // The JWX library is fine with creating a JWK for a private key (including the private exponents), so + // we want to make sure the `jwk` header (if present) does not (accidentally) contain a private key. + // That would lead to the node leaking its private key material in the resulting JWS which would be very, very bad. var jwkAsPrivateKey crypto.Signer if err := headers.JWK().Raw(&jwkAsPrivateKey); err == nil { // `err != nil` is good in this case, because that means the key is not assignable to crypto.Signer, diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index a87a343f7..4aea26947 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -281,6 +281,24 @@ func TestCrypto_SignJWS(t *testing.T) { require.NoError(t, err) auditLogs.AssertContains(t, ModuleName, "SignJWS", audit.TestActor, "Signing a JWS with key: kid") }) + t.Run("writes audit log for jwk", func(t *testing.T) { + auditLogs := audit.CaptureAuditLogs(t) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + publicKeyAsJWK, _ := jwk.FromRaw(key.Public()) + hdrs := map[string]interface{}{ + "kid": kid, + "jwk": publicKeyAsJWK, + } + + signature, err := SignJWS(audit.TestContext(), []byte{1, 2, 3}, hdrs, key, false) + + require.NoError(t, err) + auditLogs.AssertContains(t, ModuleName, "SignJWS", audit.TestActor, "Signing a JWS with key: kid") + // kid is not in headers + msg, err := jws.Parse([]byte(signature)) + assert.Empty(t, msg.Signatures()[0].ProtectedHeaders().KeyID()) + }) t.Run("returns error for not found", func(t *testing.T) { payload, _ := json.Marshal(map[string]interface{}{"iss": "nuts"}) diff --git a/crypto/memory.go b/crypto/memory.go index 31b9bdad5..56d93ab95 100644 --- a/crypto/memory.go +++ b/crypto/memory.go @@ -66,9 +66,7 @@ func (m MemoryJWTSigner) SignJWS(ctx context.Context, payload []byte, headers ma if kid != m.Key.KeyID() { return "", ErrPrivateKeyNotFound } - if _, ok := headers["jwk"]; !ok { - headers["kid"] = kid - } + headers["kid"] = kid return SignJWS(ctx, payload, headers, signer, detached) } From c8a740d5cee888d36cb3da9148117c83316872cf Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 18 Oct 2024 13:52:22 +0200 Subject: [PATCH 52/72] allow _ in subject name (#3501) --- docs/_static/vdr/v2.yaml | 4 +++- vdr/didsubject/manager.go | 2 +- vdr/didsubject/manager_test.go | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/_static/vdr/v2.yaml b/docs/_static/vdr/v2.yaml index 7566e3a9a..4ef22e9f2 100644 --- a/docs/_static/vdr/v2.yaml +++ b/docs/_static/vdr/v2.yaml @@ -482,7 +482,9 @@ components: properties: subject: type: string - description: controls the DID subject to which all created DIDs are bound. If not given, a uuid is generated and returned. + description: | + controls the DID subject to which all created DIDs are bound. If not given, a uuid is generated and returned. + The subject must follow the pattern [a-zA-Z0-9._-]+ keys: $ref: '#/components/schemas/KeyCreationOptions' DIDDocument: diff --git a/vdr/didsubject/manager.go b/vdr/didsubject/manager.go index 2ece1f276..41608c9b2 100644 --- a/vdr/didsubject/manager.go +++ b/vdr/didsubject/manager.go @@ -42,7 +42,7 @@ import ( ) // subjectPattern is a regular expression for checking whether a subject follows the allowed pattern; a-z, 0-9, -, _, . (case insensitive) -var subjectPattern = regexp.MustCompile(`^[a-zA-Z0-9.-]+$`) +var subjectPattern = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) var _ Manager = (*SqlManager)(nil) diff --git a/vdr/didsubject/manager_test.go b/vdr/didsubject/manager_test.go index 526980bce..7c9abb3c8 100644 --- a/vdr/didsubject/manager_test.go +++ b/vdr/didsubject/manager_test.go @@ -170,7 +170,17 @@ func TestManager_Create(t *testing.T) { require.Error(t, err) assert.ErrorIs(t, err, ErrSubjectValidation) - assert.ErrorContains(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9.-]+$)") + assert.ErrorContains(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9._-]+$)") + }) + t.Run("contains allowed characters", func(t *testing.T) { + db := testDB(t) + m := SqlManager{DB: db, MethodManagers: map[string]MethodManager{ + "example": testMethod{}, + }} + + _, _, err := m.Create(audit.TestContext(), DefaultCreationOptions().With(SubjectCreationOption{Subject: "subject_with-special.characters"})) + + assert.NoError(t, err) }) t.Run("contains illegal character (space)", func(t *testing.T) { db := testDB(t) @@ -182,7 +192,7 @@ func TestManager_Create(t *testing.T) { require.Error(t, err) assert.ErrorIs(t, err, ErrSubjectValidation) - assert.ErrorContains(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9.-]+$)") + assert.ErrorContains(t, err, "invalid subject (must follow pattern: ^[a-zA-Z0-9._-]+$)") }) }) } From a82ab2b6b33f3e15215c458456f354cfc50d8b26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:06:36 +0200 Subject: [PATCH 53/72] Bump github.com/nats-io/nats-server/v2 from 2.10.21 to 2.10.22 (#3505) Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.10.21 to 2.10.22. - [Release notes](https://github.com/nats-io/nats-server/releases) - [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml) - [Commits](https://github.com/nats-io/nats-server/compare/v2.10.21...v2.10.22) --- updated-dependencies: - dependency-name: github.com/nats-io/nats-server/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 5fd3bcbcf..482c709a0 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/mdp/qrterminal/v3 v3.2.0 github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-multicodec v0.9.0 - github.com/nats-io/nats-server/v2 v2.10.21 + github.com/nats-io/nats-server/v2 v2.10.22 github.com/nats-io/nats.go v1.37.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b github.com/nuts-foundation/go-did v0.15.0 @@ -128,7 +128,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.10 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect diff --git a/go.sum b/go.sum index 856c33ee7..ddb4e01c1 100644 --- a/go.sum +++ b/go.sum @@ -234,8 +234,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= -github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= -github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= @@ -336,8 +336,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE= github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= -github.com/nats-io/nats-server/v2 v2.10.21 h1:gfG6T06wBdI25XyY2IsauarOc2srWoFxxfsOKjrzoRA= -github.com/nats-io/nats-server/v2 v2.10.21/go.mod h1:I1YxSAEWbXCfy0bthwvNb5X43WwIWMz7gx5ZVPDr5Rc= +github.com/nats-io/nats-server/v2 v2.10.22 h1:Yt63BGu2c3DdMoBZNcR6pjGQwk/asrKU7VX846ibxDA= +github.com/nats-io/nats-server/v2 v2.10.22/go.mod h1:X/m1ye9NYansUXYFrbcDwUi/blHkrgHh2rgCJaakonk= github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= From e5cfc997374b460b1fa4cd829e602d4f4db6e001 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:07:53 +0200 Subject: [PATCH 54/72] Bump go.uber.org/mock from 0.4.0 to 0.5.0 (#3507) * Bump go.uber.org/mock from 0.4.0 to 0.5.0 Bumps [go.uber.org/mock](https://github.com/uber/mock) from 0.4.0 to 0.5.0. - [Release notes](https://github.com/uber/mock/releases) - [Changelog](https://github.com/uber-go/mock/blob/main/CHANGELOG.md) - [Commits](https://github.com/uber/mock/compare/v0.4.0...v0.5.0) --- updated-dependencies: - dependency-name: go.uber.org/mock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * add new mockgen to tools make target --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wout Slakhorst --- go.mod | 2 +- go.sum | 12 ++++++------ makefile | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 482c709a0..898f72f71 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( go.etcd.io/bbolt v1.3.11 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 - go.uber.org/mock v0.4.0 + go.uber.org/mock v0.5.0 golang.org/x/crypto v0.28.0 golang.org/x/time v0.7.0 google.golang.org/grpc v1.67.1 diff --git a/go.sum b/go.sum index ddb4e01c1..6aa5c6e7c 100644 --- a/go.sum +++ b/go.sum @@ -490,8 +490,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -510,8 +510,8 @@ golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88p golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -581,8 +581,8 @@ golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= diff --git a/makefile b/makefile index a9d8fed06..90ec89178 100644 --- a/makefile +++ b/makefile @@ -5,7 +5,7 @@ run-generators: gen-mocks gen-api gen-protobuf install-tools: go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.4.1 go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2 - go install go.uber.org/mock/mockgen@v0.4.0 + go install go.uber.org/mock/mockgen@v0.5.0 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0 From 0695b67cc4ce399f702d13d8021869c018b6827b Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 22 Oct 2024 12:36:29 +0200 Subject: [PATCH 55/72] fix invalid keyReference migration objects (#3504) --- crypto/crypto.go | 2 +- crypto/crypto_test.go | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crypto/crypto.go b/crypto/crypto.go index b2a4c8f7b..a592c4606 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -199,7 +199,7 @@ func (client *Crypto) Migrate() error { for _, keyNameVersion := range keys { var keyRef orm.KeyReference // find existing record, if it exists do nothing - err := tx.WithContext(ctx).Model(&orm.KeyReference{}).Where("key_name = ? and version = ?", keyNameVersion.KeyName, keyNameVersion.KeyName).First(&keyRef).Error + err := tx.WithContext(ctx).Model(&orm.KeyReference{}).Where("key_name = ? and version = ?", keyNameVersion.KeyName, keyNameVersion.Version).First(&keyRef).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // create a new key reference diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index a5eed99de..76d8ed795 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -61,12 +61,11 @@ func TestCrypto_Exists(t *testing.T) { } func TestCrypto_Migrate(t *testing.T) { - backend := NewMemoryStorage() - db := orm.NewTestDatabase(t) - client := &Crypto{backend: backend, db: db} - + keypair, _ := spi.GenerateKeyPair() t.Run("ok - 1 key migrated", func(t *testing.T) { - keypair, _ := spi.GenerateKeyPair() + backend := NewMemoryStorage() + db := orm.NewTestDatabase(t) + client := &Crypto{backend: backend, db: db} err := backend.SavePrivateKey(context.Background(), "test", keypair) require.NoError(t, err) @@ -80,9 +79,26 @@ func TestCrypto_Migrate(t *testing.T) { t.Run("ok - already exists", func(t *testing.T) { err = client.Migrate() + assert.NoError(t, err) }) }) + t.Run("don't migrate new keys", func(t *testing.T) { + backend := NewMemoryStorage() + db := orm.NewTestDatabase(t) + client := &Crypto{backend: backend, db: db} + err := backend.SavePrivateKey(context.Background(), "some-uuid", keypair) + require.NoError(t, err) + + err = db.Save(&orm.KeyReference{KID: "vm-id", KeyName: "some-uuid", Version: "1"}).Error + require.NoError(t, err) + + err = client.Migrate() + require.NoError(t, err) + + keys := client.List(context.Background()) + require.Len(t, keys, 1) + }) } func TestCrypto_New(t *testing.T) { From 4c62ff188b6c75de583e005f4aafa3c57cce7233 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:42:35 +0200 Subject: [PATCH 56/72] Bump github.com/Azure/azure-sdk-for-go/sdk/azcore from 1.15.0 to 1.16.0 (#3506) Bumps [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) from 1.15.0 to 1.16.0. - [Release notes](https://github.com/Azure/azure-sdk-for-go/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md) - [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.15.0...sdk/azcore/v1.16.0) --- updated-dependencies: - dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azcore dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 898f72f71..3b647ff95 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/nuts-foundation/nuts-node go 1.23 require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 github.com/alicebob/miniredis/v2 v2.33.0 diff --git a/go.sum b/go.sum index 6aa5c6e7c..0dc9b573c 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDr github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0 h1:eXzkOEXbSTOa7cJ7EqeCVi/OFi/ppDrUtQuttCWy74c= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.15.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= From 79b7694e7523903284c481e6dd7ec2b8db48fb16 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:35:41 +0200 Subject: [PATCH 57/72] make gen-mocks (#3509) --- auth/api/iam/jar_mock.go | 1 + auth/client/iam/mock.go | 1 + auth/contract/signer_mock.go | 3 +++ auth/mock.go | 1 + auth/services/mock.go | 3 +++ auth/services/oauth/mock.go | 2 ++ auth/services/selfsigned/types/mock.go | 1 + core/echo_mock.go | 1 + core/engine_mock.go | 10 ++++++++++ crypto/mock.go | 6 ++++++ crypto/storage/azure/mock.go | 1 + crypto/storage/spi/mock.go | 1 + didman/mock.go | 2 ++ discovery/api/server/client/mock.go | 1 + discovery/mock.go | 2 ++ events/events_mock.go | 1 + events/mock.go | 3 +++ http/echo_mock.go | 1 + network/dag/mock.go | 2 ++ network/dag/notifier_mock.go | 1 + network/mock.go | 1 + network/transport/connection_manager_mock.go | 1 + network/transport/grpc/authenticator_mock.go | 1 + network/transport/grpc/connection_list_mock.go | 1 + network/transport/grpc/connection_mock.go | 1 + network/transport/grpc/interface_mock.go | 4 ++++ network/transport/protocol_mock.go | 1 + network/transport/v2/gossip/mock.go | 1 + network/transport/v2/senders_mock.go | 1 + pki/mock.go | 3 +++ policy/mock.go | 1 + storage/mock.go | 5 +++++ vcr/holder/mock.go | 1 + vcr/holder/openid_mock.go | 1 + vcr/issuer/mock.go | 4 ++++ vcr/issuer/openid_mock.go | 1 + vcr/mock.go | 4 ++++ vcr/openid4vci/identifiers_mock.go | 1 + vcr/openid4vci/issuer_client_mock.go | 2 ++ vcr/openid4vci/wallet_client_mock.go | 1 + vcr/signature/mock.go | 1 + vcr/types/mock.go | 1 + vcr/verifier/mock.go | 2 ++ vdr/didnuts/ambassador_mock.go | 1 + vdr/didnuts/didstore/mock.go | 1 + vdr/didsubject/mock.go | 7 +++++++ vdr/mock.go | 1 + vdr/resolver/did_mock.go | 1 + vdr/resolver/finder_mock.go | 2 ++ vdr/resolver/key_mock.go | 2 ++ vdr/resolver/service_mock.go | 1 + 51 files changed, 100 insertions(+) diff --git a/auth/api/iam/jar_mock.go b/auth/api/iam/jar_mock.go index 0d5f3ffb5..a048b78b4 100644 --- a/auth/api/iam/jar_mock.go +++ b/auth/api/iam/jar_mock.go @@ -23,6 +23,7 @@ import ( type MockJAR struct { ctrl *gomock.Controller recorder *MockJARMockRecorder + isgomock struct{} } // MockJARMockRecorder is the mock recorder for MockJAR. diff --git a/auth/client/iam/mock.go b/auth/client/iam/mock.go index 338a08064..add1a059c 100644 --- a/auth/client/iam/mock.go +++ b/auth/client/iam/mock.go @@ -23,6 +23,7 @@ import ( type MockClient struct { ctrl *gomock.Controller recorder *MockClientMockRecorder + isgomock struct{} } // MockClientMockRecorder is the mock recorder for MockClient. diff --git a/auth/contract/signer_mock.go b/auth/contract/signer_mock.go index 75956eb70..982dbda0a 100644 --- a/auth/contract/signer_mock.go +++ b/auth/contract/signer_mock.go @@ -21,6 +21,7 @@ import ( type MockSigner struct { ctrl *gomock.Controller recorder *MockSignerMockRecorder + isgomock struct{} } // MockSignerMockRecorder is the mock recorder for MockSigner. @@ -86,6 +87,7 @@ func (mr *MockSignerMockRecorder) StartSigningSession(contract, params any) *gom type MockSessionPointer struct { ctrl *gomock.Controller recorder *MockSessionPointerMockRecorder + isgomock struct{} } // MockSessionPointerMockRecorder is the mock recorder for MockSessionPointer. @@ -152,6 +154,7 @@ func (mr *MockSessionPointerMockRecorder) SessionID() *gomock.Call { type MockSigningSessionResult struct { ctrl *gomock.Controller recorder *MockSigningSessionResultMockRecorder + isgomock struct{} } // MockSigningSessionResultMockRecorder is the mock recorder for MockSigningSessionResult. diff --git a/auth/mock.go b/auth/mock.go index 87c1fe580..e92db3f34 100644 --- a/auth/mock.go +++ b/auth/mock.go @@ -23,6 +23,7 @@ import ( type MockAuthenticationServices struct { ctrl *gomock.Controller recorder *MockAuthenticationServicesMockRecorder + isgomock struct{} } // MockAuthenticationServicesMockRecorder is the mock recorder for MockAuthenticationServices. diff --git a/auth/services/mock.go b/auth/services/mock.go index 10c44d006..94133bf6a 100644 --- a/auth/services/mock.go +++ b/auth/services/mock.go @@ -25,6 +25,7 @@ import ( type MockSignedToken struct { ctrl *gomock.Controller recorder *MockSignedTokenMockRecorder + isgomock struct{} } // MockSignedTokenMockRecorder is the mock recorder for MockSignedToken. @@ -77,6 +78,7 @@ func (mr *MockSignedTokenMockRecorder) SignerAttributes() *gomock.Call { type MockVPProofValueParser struct { ctrl *gomock.Controller recorder *MockVPProofValueParserMockRecorder + isgomock struct{} } // MockVPProofValueParserMockRecorder is the mock recorder for MockVPProofValueParser. @@ -129,6 +131,7 @@ func (mr *MockVPProofValueParserMockRecorder) Verify(token any) *gomock.Call { type MockContractNotary struct { ctrl *gomock.Controller recorder *MockContractNotaryMockRecorder + isgomock struct{} } // MockContractNotaryMockRecorder is the mock recorder for MockContractNotary. diff --git a/auth/services/oauth/mock.go b/auth/services/oauth/mock.go index cceaafa54..f49e2d9a1 100644 --- a/auth/services/oauth/mock.go +++ b/auth/services/oauth/mock.go @@ -23,6 +23,7 @@ import ( type MockRelyingParty struct { ctrl *gomock.Controller recorder *MockRelyingPartyMockRecorder + isgomock struct{} } // MockRelyingPartyMockRecorder is the mock recorder for MockRelyingParty. @@ -76,6 +77,7 @@ func (mr *MockRelyingPartyMockRecorder) RequestRFC003AccessToken(ctx, jwtGrantTo type MockAuthorizationServer struct { ctrl *gomock.Controller recorder *MockAuthorizationServerMockRecorder + isgomock struct{} } // MockAuthorizationServerMockRecorder is the mock recorder for MockAuthorizationServer. diff --git a/auth/services/selfsigned/types/mock.go b/auth/services/selfsigned/types/mock.go index 4707ec8b7..c24e81cff 100644 --- a/auth/services/selfsigned/types/mock.go +++ b/auth/services/selfsigned/types/mock.go @@ -20,6 +20,7 @@ import ( type MockSessionStore struct { ctrl *gomock.Controller recorder *MockSessionStoreMockRecorder + isgomock struct{} } // MockSessionStoreMockRecorder is the mock recorder for MockSessionStore. diff --git a/core/echo_mock.go b/core/echo_mock.go index 43d7fc34f..236b0899f 100644 --- a/core/echo_mock.go +++ b/core/echo_mock.go @@ -20,6 +20,7 @@ import ( type MockEchoRouter struct { ctrl *gomock.Controller recorder *MockEchoRouterMockRecorder + isgomock struct{} } // MockEchoRouterMockRecorder is the mock recorder for MockEchoRouter. diff --git a/core/engine_mock.go b/core/engine_mock.go index 7bfb0a451..34a47eef4 100644 --- a/core/engine_mock.go +++ b/core/engine_mock.go @@ -19,6 +19,7 @@ import ( type MockRoutable struct { ctrl *gomock.Controller recorder *MockRoutableMockRecorder + isgomock struct{} } // MockRoutableMockRecorder is the mock recorder for MockRoutable. @@ -54,6 +55,7 @@ func (mr *MockRoutableMockRecorder) Routes(router any) *gomock.Call { type MockRunnable struct { ctrl *gomock.Controller recorder *MockRunnableMockRecorder + isgomock struct{} } // MockRunnableMockRecorder is the mock recorder for MockRunnable. @@ -105,6 +107,7 @@ func (mr *MockRunnableMockRecorder) Start() *gomock.Call { type MockMigratable struct { ctrl *gomock.Controller recorder *MockMigratableMockRecorder + isgomock struct{} } // MockMigratableMockRecorder is the mock recorder for MockMigratable. @@ -142,6 +145,7 @@ func (mr *MockMigratableMockRecorder) Migrate() *gomock.Call { type MockConfigurable struct { ctrl *gomock.Controller recorder *MockConfigurableMockRecorder + isgomock struct{} } // MockConfigurableMockRecorder is the mock recorder for MockConfigurable. @@ -179,6 +183,7 @@ func (mr *MockConfigurableMockRecorder) Configure(config any) *gomock.Call { type MockViewableDiagnostics struct { ctrl *gomock.Controller recorder *MockViewableDiagnosticsMockRecorder + isgomock struct{} } // MockViewableDiagnosticsMockRecorder is the mock recorder for MockViewableDiagnostics. @@ -230,6 +235,7 @@ func (mr *MockViewableDiagnosticsMockRecorder) Name() *gomock.Call { type MockDiagnosable struct { ctrl *gomock.Controller recorder *MockDiagnosableMockRecorder + isgomock struct{} } // MockDiagnosableMockRecorder is the mock recorder for MockDiagnosable. @@ -267,6 +273,7 @@ func (mr *MockDiagnosableMockRecorder) Diagnostics() *gomock.Call { type MockEngine struct { ctrl *gomock.Controller recorder *MockEngineMockRecorder + isgomock struct{} } // MockEngineMockRecorder is the mock recorder for MockEngine. @@ -290,6 +297,7 @@ func (m *MockEngine) EXPECT() *MockEngineMockRecorder { type MockNamed struct { ctrl *gomock.Controller recorder *MockNamedMockRecorder + isgomock struct{} } // MockNamedMockRecorder is the mock recorder for MockNamed. @@ -327,6 +335,7 @@ func (mr *MockNamedMockRecorder) Name() *gomock.Call { type MockInjectable struct { ctrl *gomock.Controller recorder *MockInjectableMockRecorder + isgomock struct{} } // MockInjectableMockRecorder is the mock recorder for MockInjectable. @@ -378,6 +387,7 @@ func (mr *MockInjectableMockRecorder) Name() *gomock.Call { type MockHealthCheckable struct { ctrl *gomock.Controller recorder *MockHealthCheckableMockRecorder + isgomock struct{} } // MockHealthCheckableMockRecorder is the mock recorder for MockHealthCheckable. diff --git a/crypto/mock.go b/crypto/mock.go index c14177c5e..e2192cba0 100644 --- a/crypto/mock.go +++ b/crypto/mock.go @@ -23,6 +23,7 @@ import ( type MockKeyCreator struct { ctrl *gomock.Controller recorder *MockKeyCreatorMockRecorder + isgomock struct{} } // MockKeyCreatorMockRecorder is the mock recorder for MockKeyCreator. @@ -62,6 +63,7 @@ func (mr *MockKeyCreatorMockRecorder) New(ctx, namingFunc any) *gomock.Call { type MockKeyResolver struct { ctrl *gomock.Controller recorder *MockKeyResolverMockRecorder + isgomock struct{} } // MockKeyResolverMockRecorder is the mock recorder for MockKeyResolver. @@ -129,6 +131,7 @@ func (mr *MockKeyResolverMockRecorder) Resolve(ctx, kid any) *gomock.Call { type MockKeyStore struct { ctrl *gomock.Controller recorder *MockKeyStoreMockRecorder + isgomock struct{} } // MockKeyStoreMockRecorder is the mock recorder for MockKeyStore. @@ -331,6 +334,7 @@ func (mr *MockKeyStoreMockRecorder) SignJWT(ctx, claims, headers, kid any) *gomo type MockDecrypter struct { ctrl *gomock.Controller recorder *MockDecrypterMockRecorder + isgomock struct{} } // MockDecrypterMockRecorder is the mock recorder for MockDecrypter. @@ -369,6 +373,7 @@ func (mr *MockDecrypterMockRecorder) Decrypt(ctx, kid, ciphertext any) *gomock.C type MockJWTSigner struct { ctrl *gomock.Controller recorder *MockJWTSignerMockRecorder + isgomock struct{} } // MockJWTSignerMockRecorder is the mock recorder for MockJWTSigner. @@ -437,6 +442,7 @@ func (mr *MockJWTSignerMockRecorder) SignJWT(ctx, claims, headers, kid any) *gom type MockJsonWebEncryptor struct { ctrl *gomock.Controller recorder *MockJsonWebEncryptorMockRecorder + isgomock struct{} } // MockJsonWebEncryptorMockRecorder is the mock recorder for MockJsonWebEncryptor. diff --git a/crypto/storage/azure/mock.go b/crypto/storage/azure/mock.go index 7ae8262a2..dbab3ddec 100644 --- a/crypto/storage/azure/mock.go +++ b/crypto/storage/azure/mock.go @@ -22,6 +22,7 @@ import ( type MockkeyVaultClient struct { ctrl *gomock.Controller recorder *MockkeyVaultClientMockRecorder + isgomock struct{} } // MockkeyVaultClientMockRecorder is the mock recorder for MockkeyVaultClient. diff --git a/crypto/storage/spi/mock.go b/crypto/storage/spi/mock.go index ccb7e7849..13fdec073 100644 --- a/crypto/storage/spi/mock.go +++ b/crypto/storage/spi/mock.go @@ -22,6 +22,7 @@ import ( type MockStorage struct { ctrl *gomock.Controller recorder *MockStorageMockRecorder + isgomock struct{} } // MockStorageMockRecorder is the mock recorder for MockStorage. diff --git a/didman/mock.go b/didman/mock.go index 08dbe8404..6cc009d72 100644 --- a/didman/mock.go +++ b/didman/mock.go @@ -23,6 +23,7 @@ import ( type MockDidman struct { ctrl *gomock.Controller recorder *MockDidmanMockRecorder + isgomock struct{} } // MockDidmanMockRecorder is the mock recorder for MockDidman. @@ -209,6 +210,7 @@ func (mr *MockDidmanMockRecorder) UpdateEndpoint(ctx, id, serviceType, endpoint type MockCompoundServiceResolver struct { ctrl *gomock.Controller recorder *MockCompoundServiceResolverMockRecorder + isgomock struct{} } // MockCompoundServiceResolverMockRecorder is the mock recorder for MockCompoundServiceResolver. diff --git a/discovery/api/server/client/mock.go b/discovery/api/server/client/mock.go index 895718a0d..92bbd420d 100644 --- a/discovery/api/server/client/mock.go +++ b/discovery/api/server/client/mock.go @@ -21,6 +21,7 @@ import ( type MockHTTPClient struct { ctrl *gomock.Controller recorder *MockHTTPClientMockRecorder + isgomock struct{} } // MockHTTPClientMockRecorder is the mock recorder for MockHTTPClient. diff --git a/discovery/mock.go b/discovery/mock.go index 3d231317e..bd78bca13 100644 --- a/discovery/mock.go +++ b/discovery/mock.go @@ -21,6 +21,7 @@ import ( type MockServer struct { ctrl *gomock.Controller recorder *MockServerMockRecorder + isgomock struct{} } // MockServerMockRecorder is the mock recorder for MockServer. @@ -75,6 +76,7 @@ func (mr *MockServerMockRecorder) Register(context, serviceID, presentation any) type MockClient struct { ctrl *gomock.Controller recorder *MockClientMockRecorder + isgomock struct{} } // MockClientMockRecorder is the mock recorder for MockClient. diff --git a/events/events_mock.go b/events/events_mock.go index 0b1844a34..ede78e075 100644 --- a/events/events_mock.go +++ b/events/events_mock.go @@ -19,6 +19,7 @@ import ( type MockEvent struct { ctrl *gomock.Controller recorder *MockEventMockRecorder + isgomock struct{} } // MockEventMockRecorder is the mock recorder for MockEvent. diff --git a/events/mock.go b/events/mock.go index 4e7a7aef8..98512e859 100644 --- a/events/mock.go +++ b/events/mock.go @@ -21,6 +21,7 @@ import ( type MockConn struct { ctrl *gomock.Controller recorder *MockConnMockRecorder + isgomock struct{} } // MockConnMockRecorder is the mock recorder for MockConn. @@ -75,6 +76,7 @@ func (mr *MockConnMockRecorder) JetStream(opts ...any) *gomock.Call { type MockJetStreamContext struct { ctrl *gomock.Controller recorder *MockJetStreamContextMockRecorder + isgomock struct{} } // MockJetStreamContextMockRecorder is the mock recorder for MockJetStreamContext. @@ -915,6 +917,7 @@ func (mr *MockJetStreamContextMockRecorder) UpdateStream(cfg any, opts ...any) * type MockConnectionPool struct { ctrl *gomock.Controller recorder *MockConnectionPoolMockRecorder + isgomock struct{} } // MockConnectionPoolMockRecorder is the mock recorder for MockConnectionPool. diff --git a/http/echo_mock.go b/http/echo_mock.go index 284eb54e1..592634ad9 100644 --- a/http/echo_mock.go +++ b/http/echo_mock.go @@ -21,6 +21,7 @@ import ( type MockEchoServer struct { ctrl *gomock.Controller recorder *MockEchoServerMockRecorder + isgomock struct{} } // MockEchoServerMockRecorder is the mock recorder for MockEchoServer. diff --git a/network/dag/mock.go b/network/dag/mock.go index c244959af..6fceb8206 100644 --- a/network/dag/mock.go +++ b/network/dag/mock.go @@ -24,6 +24,7 @@ import ( type MockState struct { ctrl *gomock.Controller recorder *MockStateMockRecorder + isgomock struct{} } // MockStateMockRecorder is the mock recorder for MockState. @@ -337,6 +338,7 @@ func (mr *MockStateMockRecorder) XOR(reqClock any) *gomock.Call { type MockPayloadStore struct { ctrl *gomock.Controller recorder *MockPayloadStoreMockRecorder + isgomock struct{} } // MockPayloadStoreMockRecorder is the mock recorder for MockPayloadStore. diff --git a/network/dag/notifier_mock.go b/network/dag/notifier_mock.go index d21bac735..d5f9ca5ff 100644 --- a/network/dag/notifier_mock.go +++ b/network/dag/notifier_mock.go @@ -21,6 +21,7 @@ import ( type MockNotifier struct { ctrl *gomock.Controller recorder *MockNotifierMockRecorder + isgomock struct{} } // MockNotifierMockRecorder is the mock recorder for MockNotifier. diff --git a/network/mock.go b/network/mock.go index 560aea9ae..90c0ab062 100644 --- a/network/mock.go +++ b/network/mock.go @@ -24,6 +24,7 @@ import ( type MockTransactions struct { ctrl *gomock.Controller recorder *MockTransactionsMockRecorder + isgomock struct{} } // MockTransactionsMockRecorder is the mock recorder for MockTransactions. diff --git a/network/transport/connection_manager_mock.go b/network/transport/connection_manager_mock.go index cbc098971..8cf63fb48 100644 --- a/network/transport/connection_manager_mock.go +++ b/network/transport/connection_manager_mock.go @@ -22,6 +22,7 @@ import ( type MockConnectionManager struct { ctrl *gomock.Controller recorder *MockConnectionManagerMockRecorder + isgomock struct{} } // MockConnectionManagerMockRecorder is the mock recorder for MockConnectionManager. diff --git a/network/transport/grpc/authenticator_mock.go b/network/transport/grpc/authenticator_mock.go index 1d5180162..c5b9ec9ee 100644 --- a/network/transport/grpc/authenticator_mock.go +++ b/network/transport/grpc/authenticator_mock.go @@ -21,6 +21,7 @@ import ( type MockAuthenticator struct { ctrl *gomock.Controller recorder *MockAuthenticatorMockRecorder + isgomock struct{} } // MockAuthenticatorMockRecorder is the mock recorder for MockAuthenticator. diff --git a/network/transport/grpc/connection_list_mock.go b/network/transport/grpc/connection_list_mock.go index 9fa134794..4280937e3 100644 --- a/network/transport/grpc/connection_list_mock.go +++ b/network/transport/grpc/connection_list_mock.go @@ -19,6 +19,7 @@ import ( type MockConnectionList struct { ctrl *gomock.Controller recorder *MockConnectionListMockRecorder + isgomock struct{} } // MockConnectionListMockRecorder is the mock recorder for MockConnectionList. diff --git a/network/transport/grpc/connection_mock.go b/network/transport/grpc/connection_mock.go index d5cc18ae0..c428827d6 100644 --- a/network/transport/grpc/connection_mock.go +++ b/network/transport/grpc/connection_mock.go @@ -21,6 +21,7 @@ import ( type MockConnection struct { ctrl *gomock.Controller recorder *MockConnectionMockRecorder + isgomock struct{} } // MockConnectionMockRecorder is the mock recorder for MockConnection. diff --git a/network/transport/grpc/interface_mock.go b/network/transport/grpc/interface_mock.go index ca04f9a75..1c6e951c2 100644 --- a/network/transport/grpc/interface_mock.go +++ b/network/transport/grpc/interface_mock.go @@ -24,6 +24,7 @@ import ( type MockProtocol struct { ctrl *gomock.Controller recorder *MockProtocolMockRecorder + isgomock struct{} } // MockProtocolMockRecorder is the mock recorder for MockProtocol. @@ -226,6 +227,7 @@ func (mr *MockProtocolMockRecorder) Version() *gomock.Call { type MockStream struct { ctrl *gomock.Controller recorder *MockStreamMockRecorder + isgomock struct{} } // MockStreamMockRecorder is the mock recorder for MockStream. @@ -291,6 +293,7 @@ func (mr *MockStreamMockRecorder) SendMsg(m any) *gomock.Call { type MockConn struct { ctrl *gomock.Controller recorder *MockConnMockRecorder + isgomock struct{} } // MockConnMockRecorder is the mock recorder for MockConn. @@ -353,6 +356,7 @@ func (mr *MockConnMockRecorder) NewStream(ctx, desc, method any, opts ...any) *g type MockClientStream struct { ctrl *gomock.Controller recorder *MockClientStreamMockRecorder + isgomock struct{} } // MockClientStreamMockRecorder is the mock recorder for MockClientStream. diff --git a/network/transport/protocol_mock.go b/network/transport/protocol_mock.go index bda7b5f01..9748f8fd8 100644 --- a/network/transport/protocol_mock.go +++ b/network/transport/protocol_mock.go @@ -20,6 +20,7 @@ import ( type MockProtocol struct { ctrl *gomock.Controller recorder *MockProtocolMockRecorder + isgomock struct{} } // MockProtocolMockRecorder is the mock recorder for MockProtocol. diff --git a/network/transport/v2/gossip/mock.go b/network/transport/v2/gossip/mock.go index 3b3c91818..c78fdd998 100644 --- a/network/transport/v2/gossip/mock.go +++ b/network/transport/v2/gossip/mock.go @@ -21,6 +21,7 @@ import ( type MockManager struct { ctrl *gomock.Controller recorder *MockManagerMockRecorder + isgomock struct{} } // MockManagerMockRecorder is the mock recorder for MockManager. diff --git a/network/transport/v2/senders_mock.go b/network/transport/v2/senders_mock.go index fbd0177e8..5c2ad466d 100644 --- a/network/transport/v2/senders_mock.go +++ b/network/transport/v2/senders_mock.go @@ -23,6 +23,7 @@ import ( type MockmessageSender struct { ctrl *gomock.Controller recorder *MockmessageSenderMockRecorder + isgomock struct{} } // MockmessageSenderMockRecorder is the mock recorder for MockmessageSender. diff --git a/pki/mock.go b/pki/mock.go index 293a7a1fd..d8f2321aa 100644 --- a/pki/mock.go +++ b/pki/mock.go @@ -23,6 +23,7 @@ import ( type MockDenylist struct { ctrl *gomock.Controller recorder *MockDenylistMockRecorder + isgomock struct{} } // MockDenylistMockRecorder is the mock recorder for MockDenylist. @@ -114,6 +115,7 @@ func (mr *MockDenylistMockRecorder) ValidateCert(cert any) *gomock.Call { type MockValidator struct { ctrl *gomock.Controller recorder *MockValidatorMockRecorder + isgomock struct{} } // MockValidatorMockRecorder is the mock recorder for MockValidator. @@ -191,6 +193,7 @@ func (mr *MockValidatorMockRecorder) Validate(chain any) *gomock.Call { type MockProvider struct { ctrl *gomock.Controller recorder *MockProviderMockRecorder + isgomock struct{} } // MockProviderMockRecorder is the mock recorder for MockProvider. diff --git a/policy/mock.go b/policy/mock.go index d39675a56..a406fb20c 100644 --- a/policy/mock.go +++ b/policy/mock.go @@ -21,6 +21,7 @@ import ( type MockPDPBackend struct { ctrl *gomock.Controller recorder *MockPDPBackendMockRecorder + isgomock struct{} } // MockPDPBackendMockRecorder is the mock recorder for MockPDPBackend. diff --git a/storage/mock.go b/storage/mock.go index 5beb253a2..3cf42b60a 100644 --- a/storage/mock.go +++ b/storage/mock.go @@ -23,6 +23,7 @@ import ( type MockEngine struct { ctrl *gomock.Controller recorder *MockEngineMockRecorder + isgomock struct{} } // MockEngineMockRecorder is the mock recorder for MockEngine. @@ -130,6 +131,7 @@ func (mr *MockEngineMockRecorder) Start() *gomock.Call { type MockProvider struct { ctrl *gomock.Controller recorder *MockProviderMockRecorder + isgomock struct{} } // MockProviderMockRecorder is the mock recorder for MockProvider. @@ -168,6 +170,7 @@ func (mr *MockProviderMockRecorder) GetKVStore(name, class any) *gomock.Call { type Mockdatabase struct { ctrl *gomock.Controller recorder *MockdatabaseMockRecorder + isgomock struct{} } // MockdatabaseMockRecorder is the mock recorder for Mockdatabase. @@ -232,6 +235,7 @@ func (mr *MockdatabaseMockRecorder) getClass() *gomock.Call { type MockSessionDatabase struct { ctrl *gomock.Controller recorder *MockSessionDatabaseMockRecorder + isgomock struct{} } // MockSessionDatabaseMockRecorder is the mock recorder for MockSessionDatabase. @@ -286,6 +290,7 @@ func (mr *MockSessionDatabaseMockRecorder) close() *gomock.Call { type MockSessionStore struct { ctrl *gomock.Controller recorder *MockSessionStoreMockRecorder + isgomock struct{} } // MockSessionStoreMockRecorder is the mock recorder for MockSessionStore. diff --git a/vcr/holder/mock.go b/vcr/holder/mock.go index 4fcd69dfa..69db72822 100644 --- a/vcr/holder/mock.go +++ b/vcr/holder/mock.go @@ -25,6 +25,7 @@ import ( type MockWallet struct { ctrl *gomock.Controller recorder *MockWalletMockRecorder + isgomock struct{} } // MockWalletMockRecorder is the mock recorder for MockWallet. diff --git a/vcr/holder/openid_mock.go b/vcr/holder/openid_mock.go index 402f2d72c..8d6f45b65 100644 --- a/vcr/holder/openid_mock.go +++ b/vcr/holder/openid_mock.go @@ -21,6 +21,7 @@ import ( type MockOpenIDHandler struct { ctrl *gomock.Controller recorder *MockOpenIDHandlerMockRecorder + isgomock struct{} } // MockOpenIDHandlerMockRecorder is the mock recorder for MockOpenIDHandler. diff --git a/vcr/issuer/mock.go b/vcr/issuer/mock.go index 23961a608..140e95856 100644 --- a/vcr/issuer/mock.go +++ b/vcr/issuer/mock.go @@ -25,6 +25,7 @@ import ( type MockPublisher struct { ctrl *gomock.Controller recorder *MockPublisherMockRecorder + isgomock struct{} } // MockPublisherMockRecorder is the mock recorder for MockPublisher. @@ -76,6 +77,7 @@ func (mr *MockPublisherMockRecorder) PublishRevocation(ctx, revocation any) *gom type MockIssuer struct { ctrl *gomock.Controller recorder *MockIssuerMockRecorder + isgomock struct{} } // MockIssuerMockRecorder is the mock recorder for MockIssuer. @@ -159,6 +161,7 @@ func (mr *MockIssuerMockRecorder) StatusList(ctx, issuer, page any) *gomock.Call type MockStore struct { ctrl *gomock.Controller recorder *MockStoreMockRecorder + isgomock struct{} } // MockStoreMockRecorder is the mock recorder for MockStore. @@ -283,6 +286,7 @@ func (mr *MockStoreMockRecorder) StoreRevocation(r any) *gomock.Call { type MockCredentialSearcher struct { ctrl *gomock.Controller recorder *MockCredentialSearcherMockRecorder + isgomock struct{} } // MockCredentialSearcherMockRecorder is the mock recorder for MockCredentialSearcher. diff --git a/vcr/issuer/openid_mock.go b/vcr/issuer/openid_mock.go index 367ddca9c..1eb709fca 100644 --- a/vcr/issuer/openid_mock.go +++ b/vcr/issuer/openid_mock.go @@ -22,6 +22,7 @@ import ( type MockOpenIDHandler struct { ctrl *gomock.Controller recorder *MockOpenIDHandlerMockRecorder + isgomock struct{} } // MockOpenIDHandlerMockRecorder is the mock recorder for MockOpenIDHandler. diff --git a/vcr/mock.go b/vcr/mock.go index a04f6a1f5..64b76b0a4 100644 --- a/vcr/mock.go +++ b/vcr/mock.go @@ -27,6 +27,7 @@ import ( type MockFinder struct { ctrl *gomock.Controller recorder *MockFinderMockRecorder + isgomock struct{} } // MockFinderMockRecorder is the mock recorder for MockFinder. @@ -65,6 +66,7 @@ func (mr *MockFinderMockRecorder) Search(ctx, searchTerms, allowUntrusted, resol type MockTrustManager struct { ctrl *gomock.Controller recorder *MockTrustManagerMockRecorder + isgomock struct{} } // MockTrustManagerMockRecorder is the mock recorder for MockTrustManager. @@ -146,6 +148,7 @@ func (mr *MockTrustManagerMockRecorder) Untrusted(credentialType any) *gomock.Ca type MockResolver struct { ctrl *gomock.Controller recorder *MockResolverMockRecorder + isgomock struct{} } // MockResolverMockRecorder is the mock recorder for MockResolver. @@ -184,6 +187,7 @@ func (mr *MockResolverMockRecorder) Resolve(ID, resolveTime any) *gomock.Call { type MockVCR struct { ctrl *gomock.Controller recorder *MockVCRMockRecorder + isgomock struct{} } // MockVCRMockRecorder is the mock recorder for MockVCR. diff --git a/vcr/openid4vci/identifiers_mock.go b/vcr/openid4vci/identifiers_mock.go index 9bb1b76c9..6e06093d1 100644 --- a/vcr/openid4vci/identifiers_mock.go +++ b/vcr/openid4vci/identifiers_mock.go @@ -20,6 +20,7 @@ import ( type MockIdentifierResolver struct { ctrl *gomock.Controller recorder *MockIdentifierResolverMockRecorder + isgomock struct{} } // MockIdentifierResolverMockRecorder is the mock recorder for MockIdentifierResolver. diff --git a/vcr/openid4vci/issuer_client_mock.go b/vcr/openid4vci/issuer_client_mock.go index 1774ed253..370f86f84 100644 --- a/vcr/openid4vci/issuer_client_mock.go +++ b/vcr/openid4vci/issuer_client_mock.go @@ -22,6 +22,7 @@ import ( type MockIssuerAPIClient struct { ctrl *gomock.Controller recorder *MockIssuerAPIClientMockRecorder + isgomock struct{} } // MockIssuerAPIClientMockRecorder is the mock recorder for MockIssuerAPIClient. @@ -89,6 +90,7 @@ func (mr *MockIssuerAPIClientMockRecorder) RequestCredential(ctx, request, acces type MockOAuth2Client struct { ctrl *gomock.Controller recorder *MockOAuth2ClientMockRecorder + isgomock struct{} } // MockOAuth2ClientMockRecorder is the mock recorder for MockOAuth2Client. diff --git a/vcr/openid4vci/wallet_client_mock.go b/vcr/openid4vci/wallet_client_mock.go index b67b5cad6..e33eb42f1 100644 --- a/vcr/openid4vci/wallet_client_mock.go +++ b/vcr/openid4vci/wallet_client_mock.go @@ -20,6 +20,7 @@ import ( type MockWalletAPIClient struct { ctrl *gomock.Controller recorder *MockWalletAPIClientMockRecorder + isgomock struct{} } // MockWalletAPIClientMockRecorder is the mock recorder for MockWalletAPIClient. diff --git a/vcr/signature/mock.go b/vcr/signature/mock.go index 2fad7501c..a76d0f0ad 100644 --- a/vcr/signature/mock.go +++ b/vcr/signature/mock.go @@ -21,6 +21,7 @@ import ( type MockSuite struct { ctrl *gomock.Controller recorder *MockSuiteMockRecorder + isgomock struct{} } // MockSuiteMockRecorder is the mock recorder for MockSuite. diff --git a/vcr/types/mock.go b/vcr/types/mock.go index 7c0785d39..9a7533776 100644 --- a/vcr/types/mock.go +++ b/vcr/types/mock.go @@ -21,6 +21,7 @@ import ( type MockWriter struct { ctrl *gomock.Controller recorder *MockWriterMockRecorder + isgomock struct{} } // MockWriterMockRecorder is the mock recorder for MockWriter. diff --git a/vcr/verifier/mock.go b/vcr/verifier/mock.go index 5979a7bf0..59b2c7bcf 100644 --- a/vcr/verifier/mock.go +++ b/vcr/verifier/mock.go @@ -24,6 +24,7 @@ import ( type MockVerifier struct { ctrl *gomock.Controller recorder *MockVerifierMockRecorder + isgomock struct{} } // MockVerifierMockRecorder is the mock recorder for MockVerifier. @@ -134,6 +135,7 @@ func (mr *MockVerifierMockRecorder) VerifyVP(presentation, verifyVCs, allowUntru type MockStore struct { ctrl *gomock.Controller recorder *MockStoreMockRecorder + isgomock struct{} } // MockStoreMockRecorder is the mock recorder for MockStore. diff --git a/vdr/didnuts/ambassador_mock.go b/vdr/didnuts/ambassador_mock.go index 9553bffa1..0e6350a1b 100644 --- a/vdr/didnuts/ambassador_mock.go +++ b/vdr/didnuts/ambassador_mock.go @@ -19,6 +19,7 @@ import ( type MockAmbassador struct { ctrl *gomock.Controller recorder *MockAmbassadorMockRecorder + isgomock struct{} } // MockAmbassadorMockRecorder is the mock recorder for MockAmbassador. diff --git a/vdr/didnuts/didstore/mock.go b/vdr/didnuts/didstore/mock.go index febc56696..ffd4e66d9 100644 --- a/vdr/didnuts/didstore/mock.go +++ b/vdr/didnuts/didstore/mock.go @@ -22,6 +22,7 @@ import ( type MockStore struct { ctrl *gomock.Controller recorder *MockStoreMockRecorder + isgomock struct{} } // MockStoreMockRecorder is the mock recorder for MockStore. diff --git a/vdr/didsubject/mock.go b/vdr/didsubject/mock.go index 77e2250a2..101e434dd 100644 --- a/vdr/didsubject/mock.go +++ b/vdr/didsubject/mock.go @@ -23,6 +23,7 @@ import ( type MockMethodManager struct { ctrl *gomock.Controller recorder *MockMethodManagerMockRecorder + isgomock struct{} } // MockMethodManagerMockRecorder is the mock recorder for MockMethodManager. @@ -105,6 +106,7 @@ func (mr *MockMethodManagerMockRecorder) NewVerificationMethod(ctx, controller, type MockDocumentManager struct { ctrl *gomock.Controller recorder *MockDocumentManagerMockRecorder + isgomock struct{} } // MockDocumentManagerMockRecorder is the mock recorder for MockDocumentManager. @@ -156,6 +158,7 @@ func (mr *MockDocumentManagerMockRecorder) Update(ctx, id, next any) *gomock.Cal type MockManager struct { ctrl *gomock.Controller recorder *MockManagerMockRecorder + isgomock struct{} } // MockManagerMockRecorder is the mock recorder for MockManager. @@ -340,6 +343,7 @@ func (mr *MockManagerMockRecorder) UpdateService(ctx, subject, serviceID, servic type MockDocumentMigration struct { ctrl *gomock.Controller recorder *MockDocumentMigrationMockRecorder + isgomock struct{} } // MockDocumentMigrationMockRecorder is the mock recorder for MockDocumentMigration. @@ -391,6 +395,7 @@ func (mr *MockDocumentMigrationMockRecorder) MigrateDIDHistoryToSQL(id, subject, type MockCreationOptions struct { ctrl *gomock.Controller recorder *MockCreationOptionsMockRecorder + isgomock struct{} } // MockCreationOptionsMockRecorder is the mock recorder for MockCreationOptions. @@ -442,6 +447,7 @@ func (mr *MockCreationOptionsMockRecorder) With(option any) *gomock.Call { type MockCreationOption struct { ctrl *gomock.Controller recorder *MockCreationOptionMockRecorder + isgomock struct{} } // MockCreationOptionMockRecorder is the mock recorder for MockCreationOption. @@ -465,6 +471,7 @@ func (m *MockCreationOption) EXPECT() *MockCreationOptionMockRecorder { type MockDocumentOwner struct { ctrl *gomock.Controller recorder *MockDocumentOwnerMockRecorder + isgomock struct{} } // MockDocumentOwnerMockRecorder is the mock recorder for MockDocumentOwner. diff --git a/vdr/mock.go b/vdr/mock.go index d0e945d6e..ba54c789d 100644 --- a/vdr/mock.go +++ b/vdr/mock.go @@ -23,6 +23,7 @@ import ( type MockVDR struct { ctrl *gomock.Controller recorder *MockVDRMockRecorder + isgomock struct{} } // MockVDRMockRecorder is the mock recorder for MockVDR. diff --git a/vdr/resolver/did_mock.go b/vdr/resolver/did_mock.go index 7123ce35b..31476c503 100644 --- a/vdr/resolver/did_mock.go +++ b/vdr/resolver/did_mock.go @@ -20,6 +20,7 @@ import ( type MockDIDResolver struct { ctrl *gomock.Controller recorder *MockDIDResolverMockRecorder + isgomock struct{} } // MockDIDResolverMockRecorder is the mock recorder for MockDIDResolver. diff --git a/vdr/resolver/finder_mock.go b/vdr/resolver/finder_mock.go index 7d26ffe6e..705552eaa 100644 --- a/vdr/resolver/finder_mock.go +++ b/vdr/resolver/finder_mock.go @@ -20,6 +20,7 @@ import ( type MockDocFinder struct { ctrl *gomock.Controller recorder *MockDocFinderMockRecorder + isgomock struct{} } // MockDocFinderMockRecorder is the mock recorder for MockDocFinder. @@ -62,6 +63,7 @@ func (mr *MockDocFinderMockRecorder) Find(arg0 ...any) *gomock.Call { type MockPredicate struct { ctrl *gomock.Controller recorder *MockPredicateMockRecorder + isgomock struct{} } // MockPredicateMockRecorder is the mock recorder for MockPredicate. diff --git a/vdr/resolver/key_mock.go b/vdr/resolver/key_mock.go index 4d79f9687..d588fa1fe 100644 --- a/vdr/resolver/key_mock.go +++ b/vdr/resolver/key_mock.go @@ -23,6 +23,7 @@ import ( type MockKeyResolver struct { ctrl *gomock.Controller recorder *MockKeyResolverMockRecorder + isgomock struct{} } // MockKeyResolverMockRecorder is the mock recorder for MockKeyResolver. @@ -77,6 +78,7 @@ func (mr *MockKeyResolverMockRecorder) ResolveKeyByID(keyID, validAt, relationTy type MockNutsKeyResolver struct { ctrl *gomock.Controller recorder *MockNutsKeyResolverMockRecorder + isgomock struct{} } // MockNutsKeyResolverMockRecorder is the mock recorder for MockNutsKeyResolver. diff --git a/vdr/resolver/service_mock.go b/vdr/resolver/service_mock.go index 6fe0453fc..e836738ec 100644 --- a/vdr/resolver/service_mock.go +++ b/vdr/resolver/service_mock.go @@ -21,6 +21,7 @@ import ( type MockServiceResolver struct { ctrl *gomock.Controller recorder *MockServiceResolverMockRecorder + isgomock struct{} } // MockServiceResolverMockRecorder is the mock recorder for MockServiceResolver. From bfe1d26c4bb1f82282a56400093a46d2393b64d1 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 22 Oct 2024 15:08:02 +0200 Subject: [PATCH 58/72] secure outgoing http client with max connections (#3508) * secure outgoing http client with max connections * docs * test fixes * Update http/client/caching.go Co-authored-by: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> * Update docs/pages/deployment/security-considerations.rst Co-authored-by: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> * add comment --------- Co-authored-by: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> --- auth/client/iam/client.go | 10 +- auth/services/oauth/relying_party.go | 11 +- core/http_client.go | 29 ++--- core/http_client_test.go | 25 +--- didman/api/v1/client.go | 2 +- .../deployment/security-considerations.rst | 8 ++ http/client/caching.go | 4 +- http/client/client.go | 60 ++++++++- http/client/client_test.go | 122 +++++++++++++++++- http/engine.go | 2 +- network/api/v1/client.go | 2 +- pki/denylist.go | 1 + pki/validator.go | 1 + vcr/api/vcr/v2/client.go | 2 +- vcr/openid4vci/identifiers.go | 17 +-- vcr/openid4vci/issuer_client.go | 3 +- vcr/revocation/statuslist2021_verifier.go | 3 +- vcr/test/openid4vci_integration_test.go | 76 ----------- vdr/api/v1/client.go | 2 +- vdr/api/v2/client.go | 2 +- vdr/didweb/web.go | 17 +-- vdr/didweb/web_test.go | 5 - 22 files changed, 230 insertions(+), 174 deletions(-) diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go index bfcff021d..0020fea55 100644 --- a/auth/client/iam/client.go +++ b/auth/client/iam/client.go @@ -136,7 +136,7 @@ func (hb HTTPClient) RequestObjectByGet(ctx context.Context, requestURI string) return "", httpErr } - data, err := core.LimitedReadAll(response.Body) + data, err := io.ReadAll(response.Body) if err != nil { return "", fmt.Errorf("unable to read response: %w", err) } @@ -161,7 +161,7 @@ func (hb HTTPClient) RequestObjectByPost(ctx context.Context, requestURI string, return "", httpErr } - data, err := core.LimitedReadAll(response.Body) + data, err := io.ReadAll(response.Body) if err != nil { return "", fmt.Errorf("unable to read response: %w", err) } @@ -206,7 +206,7 @@ func (hb HTTPClient) AccessToken(ctx context.Context, tokenEndpoint string, data } var responseData []byte - if responseData, err = core.LimitedReadAll(response.Body); err != nil { + if responseData, err = io.ReadAll(response.Body); err != nil { return token, fmt.Errorf("unable to read response: %w", err) } if err = json.Unmarshal(responseData, &token); err != nil { @@ -271,7 +271,7 @@ func (hb HTTPClient) OpenIDConfiguration(ctx context.Context, issuerURL string) return nil, httpErr } var data []byte - if data, err = core.LimitedReadAll(response.Body); err != nil { + if data, err = io.ReadAll(response.Body); err != nil { return nil, fmt.Errorf("unable to read response: %w", err) } // kid is checked against did resolver @@ -407,7 +407,7 @@ func (hb HTTPClient) doRequest(ctx context.Context, request *http.Request, targe var data []byte - if data, err = core.LimitedReadAll(response.Body); err != nil { + if data, err = io.ReadAll(response.Body); err != nil { return fmt.Errorf("unable to read response: %w", err) } if err = json.Unmarshal(data, &target); err != nil { diff --git a/auth/services/oauth/relying_party.go b/auth/services/oauth/relying_party.go index 1b9b7819f..754f55bf1 100644 --- a/auth/services/oauth/relying_party.go +++ b/auth/services/oauth/relying_party.go @@ -22,12 +22,11 @@ import ( "context" "crypto/tls" "fmt" - "github.com/lestrrat-go/jwx/v2/jwt" - "net/http" "net/url" "strings" "time" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/auth/api/auth/v1/client" "github.com/nuts-foundation/nuts-node/auth/oauth" @@ -35,6 +34,7 @@ import ( "github.com/nuts-foundation/nuts-node/core" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/didman" + strictHttp "github.com/nuts-foundation/nuts-node/http/client" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vdr/resolver" @@ -110,12 +110,7 @@ func (s *relyingParty) RequestRFC003AccessToken(ctx context.Context, jwtGrantTok if s.strictMode && strings.ToLower(authorizationServerEndpoint.Scheme) != "https" { return nil, fmt.Errorf("authorization server endpoint must be HTTPS when in strict mode: %s", authorizationServerEndpoint.String()) } - httpClient := &http.Client{} - if s.httpClientTLS != nil { - httpClient.Transport = &http.Transport{ - TLSClientConfig: s.httpClientTLS, - } - } + httpClient := strictHttp.NewWithTLSConfig(s.httpClientTimeout, s.httpClientTLS) authClient, err := client.NewHTTPClient("", s.httpClientTimeout, client.WithHTTPClient(httpClient), client.WithRequestEditorFn(core.UserAgentRequestEditor)) if err != nil { return nil, fmt.Errorf("unable to create HTTP client: %w", err) diff --git a/core/http_client.go b/core/http_client.go index dc9aff03b..53dbc0191 100644 --- a/core/http_client.go +++ b/core/http_client.go @@ -31,21 +31,6 @@ import ( // If the response body is longer than this, it will be truncated. const HttpResponseBodyLogClipAt = 200 -// DefaultMaxHttpResponseSize is a default maximum size of an HTTP response body that will be read. -// Very large or unbounded HTTP responses can cause denial-of-service, so it's good to limit how much data is read. -// This of course heavily depends on the use case, but 1MB is a reasonable default. -const DefaultMaxHttpResponseSize = 1024 * 1024 - -// LimitedReadAll reads the given reader until the DefaultMaxHttpResponseSize is reached. -// It returns an error if more data is available than DefaultMaxHttpResponseSize. -func LimitedReadAll(reader io.Reader) ([]byte, error) { - result, err := io.ReadAll(io.LimitReader(reader, DefaultMaxHttpResponseSize+1)) - if len(result) > DefaultMaxHttpResponseSize { - return nil, fmt.Errorf("data to read exceeds max. safety limit of %d bytes", DefaultMaxHttpResponseSize) - } - return result, err -} - // HttpError describes an error returned when invoking a remote server. type HttpError struct { error @@ -63,7 +48,7 @@ func TestResponseCode(expectedStatusCode int, response *http.Response) error { // It logs using the given logger, unless nil is passed. func TestResponseCodeWithLog(expectedStatusCode int, response *http.Response, log *logrus.Entry) error { if response.StatusCode != expectedStatusCode { - responseData, _ := LimitedReadAll(response.Body) + responseData, _ := io.ReadAll(response.Body) if log != nil { // Cut off the response body to 100 characters max to prevent logging of large responses responseBodyString := string(responseData) @@ -104,16 +89,18 @@ func (w httpRequestDoerAdapter) Do(req *http.Request) (*http.Response, error) { return w.fn(req) } -// CreateHTTPClient creates a new HTTP client with the given client configuration. +// CreateHTTPInternalClient creates a new HTTP client with the given client configuration. +// This client is to be used for internal API calls (CMDs and such) // The result HTTPRequestDoer can be supplied to OpenAPI generated clients for executing requests. // This does not use the generated client options for e.g. authentication, // because each generated OpenAPI client reimplements the client options using structs, // which makes them incompatible with each other, making it impossible to use write generic client code for common traits like authorization. // If the given authorization token builder is non-nil, it calls it and passes the resulting token as bearer token with requests. -func CreateHTTPClient(cfg ClientConfig, generator AuthorizationTokenGenerator) (HTTPRequestDoer, error) { +func CreateHTTPInternalClient(cfg ClientConfig, generator AuthorizationTokenGenerator) (HTTPRequestDoer, error) { var result *httpRequestDoerAdapter client := &http.Client{} client.Timeout = cfg.Timeout + result = &httpRequestDoerAdapter{ fn: client.Do, } @@ -149,9 +136,9 @@ func CreateHTTPClient(cfg ClientConfig, generator AuthorizationTokenGenerator) ( return result, nil } -// MustCreateHTTPClient is like CreateHTTPClient but panics if it returns an error. -func MustCreateHTTPClient(cfg ClientConfig, generator AuthorizationTokenGenerator) HTTPRequestDoer { - client, err := CreateHTTPClient(cfg, generator) +// MustCreateInternalHTTPClient is like CreateHTTPInternalClient but panics if it returns an error. +func MustCreateInternalHTTPClient(cfg ClientConfig, generator AuthorizationTokenGenerator) HTTPRequestDoer { + client, err := CreateHTTPInternalClient(cfg, generator) if err != nil { panic(err) } diff --git a/core/http_client_test.go b/core/http_client_test.go index 891364b27..1bc9e3183 100644 --- a/core/http_client_test.go +++ b/core/http_client_test.go @@ -44,7 +44,7 @@ func TestHTTPClient(t *testing.T) { t.Run("no auth token", func(t *testing.T) { authToken = "" - client, err := CreateHTTPClient(ClientConfig{}, nil) + client, err := CreateHTTPInternalClient(ClientConfig{}, nil) require.NoError(t, err) req, _ := stdHttp.NewRequest(stdHttp.MethodGet, server.URL, nil) @@ -56,7 +56,7 @@ func TestHTTPClient(t *testing.T) { }) t.Run("with auth token", func(t *testing.T) { authToken = "" - client, err := CreateHTTPClient(ClientConfig{ + client, err := CreateHTTPInternalClient(ClientConfig{ Token: "test", }, nil) require.NoError(t, err) @@ -69,7 +69,7 @@ func TestHTTPClient(t *testing.T) { assert.Equal(t, "Bearer test", authToken) }) t.Run("with custom token builder", func(t *testing.T) { - client, err := CreateHTTPClient(ClientConfig{}, newLegacyTokenGenerator("test")) + client, err := CreateHTTPInternalClient(ClientConfig{}, newLegacyTokenGenerator("test")) require.NoError(t, err) req, _ := stdHttp.NewRequest(stdHttp.MethodGet, server.URL, nil) @@ -80,7 +80,7 @@ func TestHTTPClient(t *testing.T) { assert.Equal(t, "Bearer test", authToken) }) t.Run("with errored token builder", func(t *testing.T) { - client, err := CreateHTTPClient(ClientConfig{}, newErrorTokenBuilder()) + client, err := CreateHTTPInternalClient(ClientConfig{}, newErrorTokenBuilder()) require.NoError(t, err) req, _ := stdHttp.NewRequest(stdHttp.MethodGet, server.URL, nil) @@ -162,20 +162,3 @@ func newErrorTokenBuilder() AuthorizationTokenGenerator { return "", errors.New("error") } } - -func TestLimitedReadAll(t *testing.T) { - t.Run("less than limit", func(t *testing.T) { - data := strings.Repeat("a", 10) - result, err := LimitedReadAll(strings.NewReader(data)) - - assert.NoError(t, err) - assert.Equal(t, []byte(data), result) - }) - t.Run("more than limit", func(t *testing.T) { - data := strings.Repeat("a", DefaultMaxHttpResponseSize+1) - result, err := LimitedReadAll(strings.NewReader(data)) - - assert.EqualError(t, err, "data to read exceeds max. safety limit of 1048576 bytes") - assert.Nil(t, result) - }) -} diff --git a/didman/api/v1/client.go b/didman/api/v1/client.go index 6b2b1cd87..9a888b8bf 100644 --- a/didman/api/v1/client.go +++ b/didman/api/v1/client.go @@ -32,7 +32,7 @@ type HTTPClient struct { } func (hb HTTPClient) client() ClientInterface { - response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateHTTPClient(hb.ClientConfig, hb.TokenGenerator))) + response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateInternalHTTPClient(hb.ClientConfig, hb.TokenGenerator))) if err != nil { panic(err) } diff --git a/docs/pages/deployment/security-considerations.rst b/docs/pages/deployment/security-considerations.rst index 38012b22f..de201d070 100644 --- a/docs/pages/deployment/security-considerations.rst +++ b/docs/pages/deployment/security-considerations.rst @@ -19,6 +19,13 @@ D(D)oS Protection ***************** Consider implementing (D)DoS protection on the application layer for all public endpoints. +Monitor and log the following metrics: + +- Number of requests per second +- Number of requests from a single IP address +- Amount of non-20x responses + +Any outliers should be investigated. Maximum client body size for public-facing POST APIs **************************************************** @@ -32,6 +39,7 @@ The following public APIs accept POST requests: - ``/oauth2/{subjectID}/response`` To prevent malicious uploads, you MUST limit the size of the requests. +As a safeguard, the Nuts node will also limit the size of request bodies. For example, Nginx has a configuration directive to limit the size of the request body: diff --git a/http/client/caching.go b/http/client/caching.go index 5226e4674..9e4191bac 100644 --- a/http/client/caching.go +++ b/http/client/caching.go @@ -32,8 +32,8 @@ import ( // DefaultCachingTransport is a http.RoundTripper that can be used as a default transport for HTTP clients. // If caching is enabled, it will cache responses according to RFC 7234. -// If caching is disabled, it will behave like http.DefaultTransport. -var DefaultCachingTransport = http.DefaultTransport +// If caching is disabled, it will behave like our safe http.SafeHttpTransport. +var DefaultCachingTransport http.RoundTripper // maxCacheTime is the maximum time responses are cached. // Even if the server responds with a longer cache time, responses are never cached longer than maxCacheTime. diff --git a/http/client/client.go b/http/client/client.go index 5ec00bef1..60d9c57b8 100644 --- a/http/client/client.go +++ b/http/client/client.go @@ -19,23 +19,59 @@ package client import ( + "bytes" "crypto/tls" "errors" + "fmt" + "github.com/nuts-foundation/nuts-node/core" + "io" "net/http" "time" ) +// SafeHttpTransport is a http.Transport that can be used as a default transport for HTTP clients. +var SafeHttpTransport *http.Transport + func init() { - httpTransport := http.DefaultTransport.(*http.Transport) - if httpTransport.TLSClientConfig == nil { - httpTransport.TLSClientConfig = &tls.Config{} + SafeHttpTransport = http.DefaultTransport.(*http.Transport).Clone() + if SafeHttpTransport.TLSClientConfig == nil { + SafeHttpTransport.TLSClientConfig = &tls.Config{} } - httpTransport.TLSClientConfig.MinVersion = tls.VersionTLS12 + SafeHttpTransport.TLSClientConfig.MinVersion = tls.VersionTLS12 + // to prevent slow responses from public clients to have significant impact (default was unlimited) + SafeHttpTransport.MaxConnsPerHost = 5 + // set DefaultCachingTransport to SafeHttpTransport so it is set even when caching is disabled + DefaultCachingTransport = SafeHttpTransport } // StrictMode is a flag that can be set to true to enable strict mode for the HTTP client. var StrictMode bool +// DefaultMaxHttpResponseSize is a default maximum size of an HTTP response body that will be read. +// Very large or unbounded HTTP responses can cause denial-of-service, so it's good to limit how much data is read. +// This of course heavily depends on the use case, but 1MB is a reasonable default. +const DefaultMaxHttpResponseSize = 1024 * 1024 + +// limitedReadAll reads the given reader until the DefaultMaxHttpResponseSize is reached. +// It returns an error if more data is available than DefaultMaxHttpResponseSize. +func limitedReadAll(reader io.Reader) ([]byte, error) { + result, err := io.ReadAll(io.LimitReader(reader, DefaultMaxHttpResponseSize+1)) + if len(result) > DefaultMaxHttpResponseSize { + return nil, fmt.Errorf("data to read exceeds max. safety limit of %d bytes", DefaultMaxHttpResponseSize) + } + return result, err +} + +// New creates a new HTTP client with the given timeout. +func New(timeout time.Duration) *StrictHTTPClient { + return &StrictHTTPClient{ + client: &http.Client{ + Transport: SafeHttpTransport, + Timeout: timeout, + }, + } +} + // NewWithCache creates a new HTTP client with the given timeout. // It uses the DefaultCachingTransport as the underlying transport. func NewWithCache(timeout time.Duration) *StrictHTTPClient { @@ -51,7 +87,7 @@ func NewWithCache(timeout time.Duration) *StrictHTTPClient { // It copies the http.DefaultTransport and sets the TLSClientConfig to the given tls.Config. // As such, it can't be used in conjunction with the CachingRoundTripper. func NewWithTLSConfig(timeout time.Duration, tlsConfig *tls.Config) *StrictHTTPClient { - transport := http.DefaultTransport.(*http.Transport).Clone() + transport := SafeHttpTransport.Clone() transport.TLSClientConfig = tlsConfig return &StrictHTTPClient{ client: &http.Client{ @@ -69,5 +105,17 @@ func (s *StrictHTTPClient) Do(req *http.Request) (*http.Response, error) { if StrictMode && req.URL.Scheme != "https" { return nil, errors.New("strictmode is enabled, but request is not over HTTPS") } - return s.client.Do(req) + req.Header.Set("User-Agent", core.UserAgent()) + result, err := s.client.Do(req) + if err != nil { + return nil, err + } + if result.Body != nil { + body, err := limitedReadAll(result.Body) + if err != nil { + return nil, err + } + result.Body = io.NopCloser(bytes.NewReader(body)) + } + return result, nil } diff --git a/http/client/client_test.go b/http/client/client_test.go index aad3b7bd1..76d5c3d40 100644 --- a/http/client/client_test.go +++ b/http/client/client_test.go @@ -20,8 +20,14 @@ package client import ( "crypto/tls" + "fmt" "github.com/stretchr/testify/assert" - stdHttp "net/http" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "strings" + "sync" + "sync/atomic" "testing" "time" ) @@ -34,7 +40,7 @@ func TestStrictHTTPClient(t *testing.T) { StrictMode = true client := NewWithCache(time.Second) - httpRequest, _ := stdHttp.NewRequest("GET", "http://example.com", nil) + httpRequest, _ := http.NewRequest("GET", "http://example.com", nil) _, err := client.Do(httpRequest) assert.EqualError(t, err, "strictmode is enabled, but request is not over HTTPS") @@ -46,7 +52,7 @@ func TestStrictHTTPClient(t *testing.T) { StrictMode = false client := NewWithCache(time.Second) - httpRequest, _ := stdHttp.NewRequest("GET", "http://example.com", nil) + httpRequest, _ := http.NewRequest("GET", "http://example.com", nil) _, err := client.Do(httpRequest) assert.NoError(t, err) @@ -60,7 +66,7 @@ func TestStrictHTTPClient(t *testing.T) { StrictMode = true client := NewWithCache(time.Second) - httpRequest, _ := stdHttp.NewRequest("GET", "http://example.com", nil) + httpRequest, _ := http.NewRequest("GET", "http://example.com", nil) _, err := client.Do(httpRequest) assert.EqualError(t, err, "strictmode is enabled, but request is not over HTTPS") @@ -70,7 +76,7 @@ func TestStrictHTTPClient(t *testing.T) { client := NewWithTLSConfig(time.Second, &tls.Config{ InsecureSkipVerify: true, }) - ts := client.client.Transport.(*stdHttp.Transport) + ts := client.client.Transport.(*http.Transport) assert.True(t, ts.TLSClientConfig.InsecureSkipVerify) }) }) @@ -80,10 +86,114 @@ func TestStrictHTTPClient(t *testing.T) { StrictMode = true client := NewWithCache(time.Second) - httpRequest, _ := stdHttp.NewRequest("GET", "http://example.com", nil) + httpRequest, _ := http.NewRequest("GET", "http://example.com", nil) _, err := client.Do(httpRequest) assert.EqualError(t, err, "strictmode is enabled, but request is not over HTTPS") assert.Equal(t, 0, rt.invocations) }) } + +func TestLimitedReadAll(t *testing.T) { + t.Run("less than limit", func(t *testing.T) { + data := strings.Repeat("a", 10) + result, err := limitedReadAll(strings.NewReader(data)) + + assert.NoError(t, err) + assert.Equal(t, []byte(data), result) + }) + t.Run("more than limit", func(t *testing.T) { + data := strings.Repeat("a", DefaultMaxHttpResponseSize+1) + result, err := limitedReadAll(strings.NewReader(data)) + + assert.EqualError(t, err, "data to read exceeds max. safety limit of 1048576 bytes") + assert.Nil(t, result) + }) +} + +func TestMaxConns(t *testing.T) { + oldStrictMode := StrictMode + StrictMode = false + t.Cleanup(func() { StrictMode = oldStrictMode }) + // Our safe http Transport has MaxConnsPerHost = 5 + // if we request 6 resources multiple times, we expect a max connection usage of 5 + + // counter for the number of concurrent requests + var counter atomic.Int32 + + // create a test server with 6 different url handlers + handler := http.NewServeMux() + createHandler := func(id int) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + counter.Add(1) + assert.True(t, counter.Load() < 6) + _, _ = w.Write([]byte(fmt.Sprintf("%d", id))) + time.Sleep(time.Millisecond) // to allow for some parallel requests + counter.Add(-1) + } + } + handler.HandleFunc("/1", createHandler(1)) + handler.HandleFunc("/2", createHandler(2)) + handler.HandleFunc("/3", createHandler(3)) + handler.HandleFunc("/4", createHandler(4)) + handler.HandleFunc("/5", createHandler(5)) + handler.HandleFunc("/6", createHandler(6)) + + server := httptest.NewServer(handler) + defer server.Close() + client := New(time.Second) + + wg := sync.WaitGroup{} + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + request, _ := http.NewRequest("GET", fmt.Sprintf("%s/%d", server.URL, i%6), nil) + _, _ = client.Do(request) + }() + } + + wg.Wait() +} + +func TestCaching(t *testing.T) { + oldStrictMode := StrictMode + StrictMode = false + t.Cleanup(func() { StrictMode = oldStrictMode }) + // counter for the number of concurrent requests + var total atomic.Int32 + + // create a test server with 6 different url handlers + handler := http.NewServeMux() + createHandler := func(id int) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + total.Add(1) + w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", 5)) + _, _ = w.Write([]byte(fmt.Sprintf("%d", id))) + } + } + handler.HandleFunc("/1", createHandler(1)) + + server := httptest.NewServer(handler) + defer server.Close() + DefaultCachingTransport = NewCachingTransport(SafeHttpTransport, 1024*1024) + client := NewWithCache(time.Second) + + // fill cache + request, _ := http.NewRequest("GET", fmt.Sprintf("%s/1", server.URL), nil) + _, err := client.Do(request) + require.NoError(t, err) + + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + req, _ := http.NewRequest("GET", fmt.Sprintf("%s/1", server.URL), nil) + _, _ = client.Do(req) + }() + } + wg.Wait() + + assert.Equal(t, int32(1), total.Load()) +} diff --git a/http/engine.go b/http/engine.go index fa3c1f673..790f3da4b 100644 --- a/http/engine.go +++ b/http/engine.go @@ -99,7 +99,7 @@ func (h *Engine) configureClient(serverConfig core.ServerConfig) { client.StrictMode = serverConfig.Strictmode // Configure the HTTP caching client, if enabled. Set it to http.DefaultTransport so it can be used by any subsystem. if h.config.ResponseCacheSize > 0 { - client.DefaultCachingTransport = client.NewCachingTransport(http.DefaultTransport, h.config.ResponseCacheSize) + client.DefaultCachingTransport = client.NewCachingTransport(client.SafeHttpTransport, h.config.ResponseCacheSize) } } diff --git a/network/api/v1/client.go b/network/api/v1/client.go index 66469d265..a1d06a172 100644 --- a/network/api/v1/client.go +++ b/network/api/v1/client.go @@ -129,7 +129,7 @@ func (hb HTTPClient) Reprocess(contentType string) error { } func (hb HTTPClient) client() ClientInterface { - response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateHTTPClient(hb.ClientConfig, hb.TokenGenerator))) + response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateInternalHTTPClient(hb.ClientConfig, hb.TokenGenerator))) if err != nil { panic(err) } diff --git a/pki/denylist.go b/pki/denylist.go index 1f836b9a0..e1a3039fb 100644 --- a/pki/denylist.go +++ b/pki/denylist.go @@ -219,6 +219,7 @@ func (b *denylistImpl) Subscribe(f func()) { // download retrieves and parses the denylist func (b *denylistImpl) download() ([]byte, error) { // Make an HTTP GET request for the denylist URL + // We do not use our safe http client here since we're downloading from our own resource httpClient := http.Client{Timeout: syncTimeout} response, err := httpClient.Get(b.url) if err != nil { diff --git a/pki/validator.go b/pki/validator.go index 3043668a3..e8e70dbd0 100644 --- a/pki/validator.go +++ b/pki/validator.go @@ -88,6 +88,7 @@ func newRevocationList(cert *x509.Certificate) *revocationList { // newValidator returns a new PKI (crl/denylist) validator. func newValidator(config Config) (*validator, error) { + // we do not use our safe http client here since we're downloading from a trusted resource return newValidatorWithHTTPClient(config, &http.Client{Timeout: syncTimeout}) } diff --git a/vcr/api/vcr/v2/client.go b/vcr/api/vcr/v2/client.go index bb77afb38..cbf3b208e 100644 --- a/vcr/api/vcr/v2/client.go +++ b/vcr/api/vcr/v2/client.go @@ -36,7 +36,7 @@ type HTTPClient struct { } func (hb HTTPClient) client() ClientInterface { - response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateHTTPClient(hb.ClientConfig, hb.TokenGenerator))) + response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateInternalHTTPClient(hb.ClientConfig, hb.TokenGenerator))) if err != nil { panic(err) } diff --git a/vcr/openid4vci/identifiers.go b/vcr/openid4vci/identifiers.go index ffacf3cb3..37c2f4bed 100644 --- a/vcr/openid4vci/identifiers.go +++ b/vcr/openid4vci/identifiers.go @@ -23,6 +23,7 @@ import ( "fmt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/http/client" "github.com/nuts-foundation/nuts-node/vcr/log" "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/http" @@ -143,12 +144,8 @@ func (t tlsIdentifierResolver) resolveFromCertificate(id did.DID) (string, error } // Resolve URLs - httpTransport := http.DefaultTransport.(*http.Transport).Clone() - httpTransport.TLSClientConfig = t.config - httpClient := &http.Client{ - Timeout: 5 * time.Second, - Transport: httpTransport, - } + httpClient := client.NewWithTLSConfig(5*time.Second, t.config) + for _, candidateURL := range candidateURLs { issuerIdentifier := core.JoinURLPaths(candidateURL, "n2n", "identity", url.PathEscape(id.String())) err := t.testIdentifier(issuerIdentifier, httpClient) @@ -161,9 +158,13 @@ func (t tlsIdentifierResolver) resolveFromCertificate(id did.DID) (string, error return "", nil } -func (t tlsIdentifierResolver) testIdentifier(issuerIdentifier string, httpClient *http.Client) error { +func (t tlsIdentifierResolver) testIdentifier(issuerIdentifier string, httpClient core.HTTPRequestDoer) error { metadataURL := core.JoinURLPaths(issuerIdentifier, CredentialIssuerMetadataWellKnownPath) - httpResponse, err := httpClient.Head(metadataURL) + request, err := http.NewRequest(http.MethodHead, metadataURL, nil) + if err != nil { + return err + } + httpResponse, err := httpClient.Do(request) if err != nil { return err } diff --git a/vcr/openid4vci/issuer_client.go b/vcr/openid4vci/issuer_client.go index 91ab1eafd..c355aa96d 100644 --- a/vcr/openid4vci/issuer_client.go +++ b/vcr/openid4vci/issuer_client.go @@ -28,6 +28,7 @@ import ( "github.com/nuts-foundation/nuts-node/auth/oauth" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/vcr/log" + "io" "net/http" "net/http/httptrace" "net/url" @@ -167,7 +168,7 @@ func httpDo(httpClient core.HTTPRequestDoer, httpRequest *http.Request, result i return fmt.Errorf("http request error: %w", err) } defer httpResponse.Body.Close() - responseBody, err := core.LimitedReadAll(httpResponse.Body) + responseBody, err := io.ReadAll(httpResponse.Body) if err != nil { return fmt.Errorf("read error (%s): %w", httpRequest.URL, err) } diff --git a/vcr/revocation/statuslist2021_verifier.go b/vcr/revocation/statuslist2021_verifier.go index a1eeb89a2..4071ac54c 100644 --- a/vcr/revocation/statuslist2021_verifier.go +++ b/vcr/revocation/statuslist2021_verifier.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "strconv" "time" @@ -198,7 +199,7 @@ func (cs *StatusList2021) download(statusListCredential string) (*vc.VerifiableC Debug("Failed to close response body") } }() - body, err := core.LimitedReadAll(res.Body) // default minimum size is 16kb (PII entropy), so 1mb is already unlikely + body, err := io.ReadAll(res.Body) if res.StatusCode > 299 || err != nil { return nil, errors.Join(fmt.Errorf("fetching StatusList2021Credential from '%s' failed", statusListCredential), err) } diff --git a/vcr/test/openid4vci_integration_test.go b/vcr/test/openid4vci_integration_test.go index 769d8e12b..3e90ed076 100644 --- a/vcr/test/openid4vci_integration_test.go +++ b/vcr/test/openid4vci_integration_test.go @@ -23,7 +23,6 @@ import ( "encoding/json" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/jsonld" - "github.com/nuts-foundation/nuts-node/network/log" "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vcr/openid4vci" "github.com/nuts-foundation/nuts-node/vdr/didsubject" @@ -31,9 +30,7 @@ import ( "github.com/stretchr/testify/assert" "io" "net/http" - "net/http/httptrace" "net/url" - "sync" "testing" "time" @@ -80,79 +77,6 @@ func TestOpenID4VCIHappyFlow(t *testing.T) { }, 5*time.Second, "credential not retrieved by holder") } -func TestOpenID4VCIConnectionReuse(t *testing.T) { - // default http.Transport has MaxConnsPerHost=100, - // but we need to adjust it to something lower, so we can assert connection reuse - const maxConnsPerHost = 2 - // for 2 http.Transport instance (one for issuer, one for wallet), - // so we expect max maxConnsPerHost*2 connections in total. - const maxExpectedConnCount = maxConnsPerHost * 2 - http.DefaultTransport.(*http.Transport).MaxConnsPerHost = maxConnsPerHost - - ctx := audit.TestContext() - _, baseURL, system := node.StartServer(t) - vcrService := system.FindEngineByName("vcr").(vcr.VCR) - - issuerDID := registerDID(t, system) - registerBaseURL(t, baseURL, system, issuerDID) - holderDID := registerDID(t, system) - registerBaseURL(t, baseURL, system, holderDID) - - credential := testCredential() - credential.Issuer = issuerDID.URI() - credential.ID, _ = ssi.ParseURI(issuerDID.URI().String() + "#1") - credential.CredentialSubject = append(credential.CredentialSubject, map[string]interface{}{ - "id": holderDID.URI().String(), - "purposeOfUse": "test", - }) - - newConns := map[string]int{} - mux := sync.Mutex{} - openid4vci.HttpClientTrace = &httptrace.ClientTrace{ - ConnectStart: func(network, addr string) { - log.Logger().Infof("Conn: %s/%s", network, addr) - mux.Lock() - defer mux.Unlock() - newConns[network+"/"+addr]++ - }, - } - - const numCreds = 10 - errChan := make(chan error, numCreds) - wg := sync.WaitGroup{} - for i := 0; i < numCreds; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _, err := vcrService.Issuer().Issue(ctx, credential, issuer.CredentialOptions{ - Publish: true, - Public: false, - }) - if err != nil { - errChan <- err - return - } - }() - } - - wg.Wait() - // Drain errs channel, non-blocking - close(errChan) - var errs []string - for { - err := <-errChan - if err == nil { - break - - } - errs = append(errs, err.Error()) - } - assert.Empty(t, errs, "error issuing credential") - for host, v := range newConns { - assert.LessOrEqualf(t, v, maxExpectedConnCount, "number of created HTTP connections should be at most %d for host %s", maxConnsPerHost, host) - } -} - // TestOpenID4VCIDisabled tests the issuer won't try to issue over OpenID4VCI when it's disabled. func TestOpenID4VCIDisabled(t *testing.T) { _, baseURL, system := node.StartServer(t, func(_, _ string) { diff --git a/vdr/api/v1/client.go b/vdr/api/v1/client.go index 93db80fb6..66f76eeef 100644 --- a/vdr/api/v1/client.go +++ b/vdr/api/v1/client.go @@ -36,7 +36,7 @@ type HTTPClient struct { } func (hb HTTPClient) client() ClientInterface { - response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateHTTPClient(hb.ClientConfig, hb.TokenGenerator))) + response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateInternalHTTPClient(hb.ClientConfig, hb.TokenGenerator))) if err != nil { panic(err) } diff --git a/vdr/api/v2/client.go b/vdr/api/v2/client.go index 2d944f044..4fd3b7b0f 100644 --- a/vdr/api/v2/client.go +++ b/vdr/api/v2/client.go @@ -34,7 +34,7 @@ type HTTPClient struct { } func (hb HTTPClient) client() ClientInterface { - response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateHTTPClient(hb.ClientConfig, hb.TokenGenerator))) + response, err := NewClientWithResponses(hb.GetAddress(), WithHTTPClient(core.MustCreateInternalHTTPClient(hb.ClientConfig, hb.TokenGenerator))) if err != nil { panic(err) } diff --git a/vdr/didweb/web.go b/vdr/didweb/web.go index 50d9fe50f..25f78ca10 100644 --- a/vdr/didweb/web.go +++ b/vdr/didweb/web.go @@ -25,6 +25,7 @@ import ( "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/http/client" "github.com/nuts-foundation/nuts-node/vdr/resolver" + "io" "mime" "net/http" "time" @@ -37,16 +38,13 @@ var _ resolver.DIDResolver = (*Resolver)(nil) // Resolver is a DID resolver for the did:web method. type Resolver struct { - HttpClient *http.Client + HttpClient core.HTTPRequestDoer } // NewResolver creates a new did:web Resolver with default TLS configuration. func NewResolver() *Resolver { return &Resolver{ - HttpClient: &http.Client{ - Transport: client.DefaultCachingTransport, - Timeout: 5 * time.Second, - }, + HttpClient: client.NewWithCache(5 * time.Second), } } @@ -68,11 +66,14 @@ func (w Resolver) Resolve(id did.DID, _ *resolver.ResolveMetadata) (*did.Documen targetURL := baseURL.String() // TODO: Support DNS over HTTPS (DOH), https://www.rfc-editor.org/rfc/rfc8484 - httpResponse, err := w.HttpClient.Get(targetURL) + request, err := http.NewRequest(http.MethodGet, targetURL, nil) + if err != nil { + return nil, nil, err + } + httpResponse, err := w.HttpClient.Do(request) if err != nil { return nil, nil, fmt.Errorf("did:web HTTP error: %w", err) } - defer httpResponse.Body.Close() if !(httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300) { return nil, nil, fmt.Errorf("did:web non-ok HTTP status: %s", httpResponse.Status) } @@ -98,7 +99,7 @@ func (w Resolver) Resolve(id did.DID, _ *resolver.ResolveMetadata) (*did.Documen } // Read document - data, err := core.LimitedReadAll(httpResponse.Body) + data, err := io.ReadAll(httpResponse.Body) if err != nil { return nil, nil, fmt.Errorf("did:web HTTP response read error: %w", err) } diff --git a/vdr/didweb/web_test.go b/vdr/didweb/web_test.go index 519464be3..b887de409 100644 --- a/vdr/didweb/web_test.go +++ b/vdr/didweb/web_test.go @@ -20,7 +20,6 @@ package didweb import ( "github.com/nuts-foundation/go-did/did" - "github.com/nuts-foundation/nuts-node/http/client" http2 "github.com/nuts-foundation/nuts-node/test/http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,10 +54,6 @@ const didDocTemplate = ` func TestResolver_NewResolver(t *testing.T) { resolver := NewResolver() assert.NotNil(t, resolver.HttpClient) - - t.Run("it uses cached transport", func(t *testing.T) { - assert.Same(t, client.DefaultCachingTransport, resolver.HttpClient.Transport) - }) } func TestResolver_Resolve(t *testing.T) { From 08ad0e587cbb4375e7b55ce27a76609510ada0d0 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 22 Oct 2024 15:09:04 +0200 Subject: [PATCH 59/72] remove network migration and optimize network event retry (#3510) * remove network migration and optimize network event retry * more cleanup --- network/dag/dag.go | 91 -------------------------------------- network/dag/dag_test.go | 98 ----------------------------------------- network/dag/notifier.go | 17 ++++--- network/dag/state.go | 2 +- network/interface.go | 2 - network/mock.go | 14 ------ vcr/ambassador.go | 4 +- vcr/test.go | 1 - vdr/vdr.go | 10 ----- vdr/vdr_test.go | 36 --------------- 10 files changed, 13 insertions(+), 262 deletions(-) diff --git a/network/dag/dag.go b/network/dag/dag.go index 05ad9e77b..a8e626001 100644 --- a/network/dag/dag.go +++ b/network/dag/dag.go @@ -96,69 +96,6 @@ func newDAG(db stoabs.KVStore) *dag { return &dag{db: db} } -func (d *dag) Migrate() error { - return d.db.Write(context.Background(), func(tx stoabs.WriteTx) error { - writer := tx.GetShelfWriter(metadataShelf) - // Migrate highest LC value - // Todo: remove after V5 release - _, err := writer.Get(stoabs.BytesKey(highestClockValue)) - if errors.Is(err, stoabs.ErrKeyNotFound) { - log.Logger().Info("Highest LC value not stored, migrating...") - highestLC := d.getHighestClockLegacy(tx) - err = d.setHighestClockValue(tx, highestLC) - } - if err != nil { - return err - } - - // Migrate number of TXs - // Todo: remove after V5 release - _, err = writer.Get(stoabs.BytesKey(numberOfTransactionsKey)) - if errors.Is(err, stoabs.ErrKeyNotFound) { - log.Logger().Info("Number of transactions not stored, migrating...") - numberOfTXs := d.getNumberOfTransactionsLegacy(tx) - err = d.setNumberOfTransactions(tx, numberOfTXs) - } - if err != nil { - return err - } - - // Migrate headsLegacy to single head - // Todo: remove after V6 release => then remove headsShelf - _, err = writer.Get(stoabs.BytesKey(headRefKey)) - if errors.Is(err, stoabs.ErrKeyNotFound) { - log.Logger().Info("Head not stored in metadata, migrating...") - heads := d.headsLegacy(tx) - err = nil // reset error - if len(heads) != 0 { // ignore for empty node - var latestHead hash.SHA256Hash - var latestLC uint32 - - for _, ref := range heads { - transaction, err := getTransaction(ref, tx) - if err != nil { - if errors.Is(err, ErrTransactionNotFound) { - return fmt.Errorf("database migration failed: %w (%s=%s)", err, core.LogFieldTransactionRef, ref) - } - return err - } - if transaction.Clock() >= latestLC { - latestHead = ref - latestLC = transaction.Clock() - } - } - - err = d.setHead(tx, latestHead) - } - } - if err != nil { - return err - } - - return nil - }) -} - func (d *dag) diagnostics(ctx context.Context) []core.DiagnosticResult { var stats Statistics _ = d.db.Read(ctx, func(tx stoabs.ReadTx) error { @@ -296,27 +233,6 @@ func (d dag) getHighestClockValue(tx stoabs.ReadTx) uint32 { return bytesToClock(value) } -// getHighestClockLegacy is used for migration. -// Remove after V5 or V6 release? -func (d dag) getHighestClockLegacy(tx stoabs.ReadTx) uint32 { - reader := tx.GetShelfReader(clockShelf) - var clock uint32 - err := reader.Iterate(func(key stoabs.Key, _ []byte) error { - currentClock := uint32(key.(stoabs.Uint32Key)) - if currentClock > clock { - clock = currentClock - } - return nil - }, stoabs.Uint32Key(0)) - if err != nil { - log.Logger(). - WithError(err). - Error("Failed to read clock shelf") - return 0 - } - return clock -} - func (d dag) getHead(tx stoabs.ReadTx) (hash.SHA256Hash, error) { head, err := tx.GetShelfReader(metadataShelf).Get(stoabs.BytesKey(headRefKey)) if errors.Is(err, stoabs.ErrKeyNotFound) { @@ -329,13 +245,6 @@ func (d dag) getHead(tx stoabs.ReadTx) (hash.SHA256Hash, error) { return hash.FromSlice(head), nil } -// getNumberOfTransactionsLegacy is used for migration. -// Remove after V5 or V6 release? -func (d dag) getNumberOfTransactionsLegacy(tx stoabs.ReadTx) uint64 { - reader := tx.GetShelfReader(transactionsShelf) - return uint64(reader.Stats().NumEntries) -} - func (d dag) setHighestClockValue(tx stoabs.WriteTx, count uint32) error { writer := tx.GetShelfWriter(metadataShelf) bytes := make([]byte, 4) diff --git a/network/dag/dag_test.go b/network/dag/dag_test.go index 935f85691..4b072621f 100644 --- a/network/dag/dag_test.go +++ b/network/dag/dag_test.go @@ -107,104 +107,6 @@ func TestDAG_Get(t *testing.T) { }) } -func TestDAG_Migrate(t *testing.T) { - ctx := context.Background() - txRoot := CreateTestTransactionWithJWK(0) - tx1 := CreateTestTransactionWithJWK(1, txRoot) - tx2 := CreateTestTransactionWithJWK(2, tx1) - - t.Run("migrate LC value and transaction count to metadata storage", func(t *testing.T) { - graph := CreateDAG(t) - - // Setup: add transactions, remove metadata - addTx(t, graph, txRoot, tx1, tx2) - err := graph.db.WriteShelf(ctx, metadataShelf, func(writer stoabs.Writer) error { - return writer.Iterate(func(key stoabs.Key, _ []byte) error { - return writer.Delete(key) - }, stoabs.BytesKey{}) - }) - require.NoError(t, err) - - // Check values return 0 - var stats Statistics - var lc uint32 - _ = graph.db.Read(ctx, func(tx stoabs.ReadTx) error { - stats = graph.statistics(tx) - lc = graph.getHighestClockValue(tx) - return nil - }) - assert.Equal(t, uint(0), stats.NumberOfTransactions) - assert.Equal(t, uint32(0), lc) - - // Migrate - err = graph.Migrate() - require.NoError(t, err) - - // Assert - _ = graph.db.Read(ctx, func(tx stoabs.ReadTx) error { - stats = graph.statistics(tx) - lc = graph.getHighestClockValue(tx) - return nil - }) - assert.Equal(t, uint(3), stats.NumberOfTransactions) - assert.Equal(t, tx2.Clock(), lc) - }) - t.Run("migrate head to metadata storage", func(t *testing.T) { - graph := CreateDAG(t) - - // Setup: add transactions, remove metadata, add to headsShelf - addTx(t, graph, txRoot, tx1, tx2) - err := graph.db.WriteShelf(ctx, metadataShelf, func(writer stoabs.Writer) error { - return writer.Iterate(func(key stoabs.Key, _ []byte) error { - return writer.Delete(key) - }, stoabs.BytesKey{}) - }) - require.NoError(t, err) - err = graph.db.WriteShelf(ctx, headsShelf, func(writer stoabs.Writer) error { - _ = writer.Put(stoabs.BytesKey(txRoot.Ref().Slice()), []byte{1}) - _ = writer.Put(stoabs.BytesKey(tx2.Ref().Slice()), []byte{1}) - return writer.Put(stoabs.BytesKey(tx1.Ref().Slice()), []byte{1}) - }) - require.NoError(t, err) - - // Check current head is nil - var head hash.SHA256Hash - _ = graph.db.Read(ctx, func(tx stoabs.ReadTx) error { - head, _ = graph.getHead(tx) - return nil - }) - assert.Equal(t, hash.EmptyHash(), head) - - // Migrate - err = graph.Migrate() - require.NoError(t, err) - - // Assert - _ = graph.db.Read(ctx, func(tx stoabs.ReadTx) error { - head, _ = graph.getHead(tx) - return nil - }) - assert.Equal(t, tx2.Ref(), head) - }) - t.Run("nothing to migrate", func(t *testing.T) { - graph := CreateDAG(t) - addTx(t, graph, txRoot, tx1, tx2) - - err := graph.Migrate() - require.NoError(t, err) - - stats := Statistics{} - var lc uint32 - _ = graph.db.Read(ctx, func(tx stoabs.ReadTx) error { - stats = graph.statistics(tx) - lc = graph.getHighestClockValue(tx) - return nil - }) - assert.Equal(t, uint(3), stats.NumberOfTransactions) - assert.Equal(t, tx2.Clock(), lc) - }) -} - func TestDAG_Add(t *testing.T) { ctx := context.Background() t.Run("ok", func(t *testing.T) { diff --git a/network/dag/notifier.go b/network/dag/notifier.go index 675b3882b..4b6fa4890 100644 --- a/network/dag/notifier.go +++ b/network/dag/notifier.go @@ -243,6 +243,7 @@ func (p *notifier) Run() error { } // we're going to retry all events synchronously at startup. For the ones that fail we'll start the retry loop failedAtStartup := make([]Event, 0) + readyToRetry := make([]Event, 0) err := p.db.ReadShelf(p.ctx, p.shelfName(), func(reader stoabs.Reader) error { return reader.Iterate(func(k stoabs.Key, v []byte) error { event := Event{} @@ -253,12 +254,7 @@ func (p *notifier) Run() error { return nil } - if err := p.notifyNow(event); err != nil { - if event.Retries < maxRetries { - failedAtStartup = append(failedAtStartup, event) - } - } - + readyToRetry = append(readyToRetry, event) return nil }, stoabs.BytesKey{}) }) @@ -266,6 +262,15 @@ func (p *notifier) Run() error { return err } + // do outside of main loop to prevent long running read + for _, event := range readyToRetry { + if err := p.notifyNow(event); err != nil { + if event.Retries < maxRetries { + failedAtStartup = append(failedAtStartup, event) + } + } + } + // for all events from failedAtStartup, call retry // this may still produce errors in the logs or even duplicate errors since notifyNow also failed // but rather duplicate errors then errors produced from overloading the DB with transactions diff --git a/network/dag/state.go b/network/dag/state.go index 81c2edf51..fe60a7452 100644 --- a/network/dag/state.go +++ b/network/dag/state.go @@ -60,7 +60,7 @@ type state struct { } func (s *state) Migrate() error { - return s.graph.Migrate() + return nil } // NewState returns a new State. The State is used as entry point, it's methods will start transactions and will notify observers from within those transactions. diff --git a/network/interface.go b/network/interface.go index aef5ed4cf..847d6e58e 100644 --- a/network/interface.go +++ b/network/interface.go @@ -37,8 +37,6 @@ type Transactions interface { Subscribe(name string, receiver dag.ReceiverFn, filters ...SubscriberOption) error // Subscribers returns the list of notifiers on the DAG that emit events to subscribers. Subscribers() []dag.Notifier - // CleanupSubscriberEvents removes events. Example use is cleaning up events that errored but should be removed due to a bugfix. - CleanupSubscriberEvents(subcriberName, errorPrefix string) error // GetTransactionPayload retrieves the transaction Payload for the given transaction. // If the transaction or Payload is not found, dag.ErrPayloadNotFound is returned. GetTransactionPayload(transactionRef hash.SHA256Hash) ([]byte, error) diff --git a/network/mock.go b/network/mock.go index 90c0ab062..4a2c5c8ec 100644 --- a/network/mock.go +++ b/network/mock.go @@ -58,20 +58,6 @@ func (mr *MockTransactionsMockRecorder) AddressBook() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddressBook", reflect.TypeOf((*MockTransactions)(nil).AddressBook)) } -// CleanupSubscriberEvents mocks base method. -func (m *MockTransactions) CleanupSubscriberEvents(subcriberName, errorPrefix string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CleanupSubscriberEvents", subcriberName, errorPrefix) - ret0, _ := ret[0].(error) - return ret0 -} - -// CleanupSubscriberEvents indicates an expected call of CleanupSubscriberEvents. -func (mr *MockTransactionsMockRecorder) CleanupSubscriberEvents(subcriberName, errorPrefix any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupSubscriberEvents", reflect.TypeOf((*MockTransactions)(nil).CleanupSubscriberEvents), subcriberName, errorPrefix) -} - // CreateTransaction mocks base method. func (m *MockTransactions) CreateTransaction(ctx context.Context, spec Template) (dag.Transaction, error) { m.ctrl.T.Helper() diff --git a/vcr/ambassador.go b/vcr/ambassador.go index 817e10317..63dd27481 100644 --- a/vcr/ambassador.go +++ b/vcr/ambassador.go @@ -99,9 +99,7 @@ func (n ambassador) Start() error { return fmt.Errorf("failed to subscribe to REPROCESS event stream: %v", err) } - // removing failed events required for #1743 - // remove after v6 release - return n.networkClient.CleanupSubscriberEvents("vcr_vcs", "canonicalization failed: unable to normalize the json-ld document: loading remote context failed: Dereferencing a URL did not result in a valid JSON-LD context") + return nil } func (n ambassador) handleNetworkVCs(event dag.Event) (bool, error) { diff --git a/vcr/test.go b/vcr/test.go index c67fbffd6..845225887 100644 --- a/vcr/test.go +++ b/vcr/test.go @@ -176,7 +176,6 @@ func newMockContext(t *testing.T) mockContext { tx.EXPECT().WithPersistency().AnyTimes() tx.EXPECT().Subscribe("vcr_vcs", gomock.Any(), gomock.Any()) tx.EXPECT().Subscribe("vcr_revocations", gomock.Any(), gomock.Any()) - tx.EXPECT().CleanupSubscriberEvents("vcr_vcs", gomock.Any()) tx.EXPECT().Disabled().AnyTimes() didResolver := resolver.NewMockDIDResolver(ctrl) documentOwner := didsubject.NewMockDocumentOwner(ctrl) diff --git a/vdr/vdr.go b/vdr/vdr.go index 0672bf339..f1c828937 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -221,16 +221,6 @@ func (r *Module) Start() error { return err } - // VDR migration needs to be started after ambassador has started! - count, err := r.store.DocumentCount() - if err != nil { - return err - } - if count == 0 { - // remove after v6 release - _, err = r.network.Reprocess(context.Background(), "application/did+json") - } - // start DID Document rollback loop r.routines.Add(1) go func() { diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index 1c57bdc8f..36382e69d 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -25,7 +25,6 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" - "errors" "github.com/lestrrat-go/jwx/v2/jwk" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" @@ -107,41 +106,6 @@ func TestNewVDR(t *testing.T) { assert.IsType(t, &Module{}, vdr) } -func TestVDR_Start(t *testing.T) { - t.Run("migration", func(t *testing.T) { - t.Run("migrate on 0 document count", func(t *testing.T) { - ctx := newVDRTestCtx(t) - ctx.mockAmbassador.EXPECT().Start() - ctx.mockStore.EXPECT().DocumentCount().Return(uint(0), nil) - ctx.mockNetwork.EXPECT().Reprocess(context.Background(), "application/did+json").Return(nil, nil) - - err := ctx.vdr.Start() - - require.NoError(t, err) - }) - t.Run("don't migrate on > 0 document count", func(t *testing.T) { - ctx := newVDRTestCtx(t) - ctx.mockAmbassador.EXPECT().Start() - ctx.mockStore.EXPECT().DocumentCount().Return(uint(1), nil) - - err := ctx.vdr.Start() - - require.NoError(t, err) - }) - t.Run("error on migration error", func(t *testing.T) { - ctx := newVDRTestCtx(t) - ctx.mockAmbassador.EXPECT().Start() - testError := errors.New("test") - ctx.mockStore.EXPECT().DocumentCount().Return(uint(0), testError) - - err := ctx.vdr.Start() - - assert.Equal(t, testError, err) - }) - }) - -} - func TestVDR_ConflictingDocuments(t *testing.T) { t.Run("diagnostics", func(t *testing.T) { From 5b0dd1e62e1f2af87d6b1400758f5db8ed587a98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:16:42 +0200 Subject: [PATCH 60/72] Bump github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys (#3511) Bumps [github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys](https://github.com/Azure/azure-sdk-for-go) from 1.1.0 to 1.2.0. - [Release notes](https://github.com/Azure/azure-sdk-for-go/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md) - [Commits](https://github.com/Azure/azure-sdk-for-go/compare/v1.1...v1.2) --- updated-dependencies: - dependency-name: github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 3b647ff95..adbfc34b3 100644 --- a/go.mod +++ b/go.mod @@ -67,8 +67,8 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/PaesslerAG/gval v1.2.2 // indirect github.com/alexandrevicenzi/go-sse v1.6.0 // indirect diff --git a/go.sum b/go.sum index 0dc9b573c..a148a0413 100644 --- a/go.sum +++ b/go.sum @@ -17,11 +17,11 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aov github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.2.0 h1:fKSH2aGSnt/ibGvis2f0gD3Lp10bzKr92FQxKutoNDc= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.2.0/go.mod h1:TPR8de/4RyFL97NXnpjtIaLZFR0eQxlQeXbk7EKIrbw= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= From 95ca0ae84de1217d2e41adfd8ca07b53730e971e Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 23 Oct 2024 13:17:44 +0200 Subject: [PATCH 61/72] fix duplicate search results for wildcard param (#3512) --- discovery/store.go | 12 ++++++++++-- discovery/store_test.go | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/discovery/store.go b/discovery/store.go index 9f7ae7e79..2f6ddd907 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -26,6 +26,7 @@ import ( "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/vcr/credential/store" "slices" + "strings" "time" "github.com/google/uuid" @@ -271,9 +272,16 @@ func (s *sqlStore) search(serviceID string, query map[string]string, allowUnvali if !allowUnvalidated { stmt = stmt.Where("validated != 0") } - if len(query) > 0 { + // remove wildcards to prevent unneeded join on credential + filteredQuery := make(map[string]string) + for k, v := range query { + if strings.TrimSpace(v) != "*" { + filteredQuery[k] = v + } + } + if len(filteredQuery) > 0 { stmt = stmt.Joins("inner join discovery_credential ON discovery_credential.presentation_id = discovery_presentation.id") - stmt = store.CredentialStore{}.BuildSearchStatement(stmt, "discovery_credential.credential_id", query) + stmt = store.CredentialStore{}.BuildSearchStatement(stmt, "discovery_credential.credential_id", filteredQuery) } var matches []presentationRecord diff --git a/discovery/store_test.go b/discovery/store_test.go index 9daa0c9f6..7df338a6e 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -244,6 +244,16 @@ func Test_sqlStore_search(t *testing.T) { require.NoError(t, err) require.Len(t, actualVPs, 2) + t.Run("wildcard", func(t *testing.T) { + actualVPs, err = c.search(testServiceID, map[string]string{"credentialSubject.person.givenName": "*"}, true) + require.NoError(t, err) + require.Len(t, actualVPs, 2) + }) + t.Run("wildcard postfix", func(t *testing.T) { + actualVPs, err = c.search(testServiceID, map[string]string{"credentialSubject.person.givenName": "A*"}, true) + require.NoError(t, err) + require.Len(t, actualVPs, 1) + }) t.Run("validated", func(t *testing.T) { actualVPs, err = c.search(testServiceID, map[string]string{}, false) require.NoError(t, err) From 7676a153325a92f0bd708121b03d758671f8e7a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:40:54 +0200 Subject: [PATCH 62/72] Bump github.com/chromedp/chromedp from 0.11.0 to 0.11.1 (#3514) Bumps [github.com/chromedp/chromedp](https://github.com/chromedp/chromedp) from 0.11.0 to 0.11.1. - [Release notes](https://github.com/chromedp/chromedp/releases) - [Commits](https://github.com/chromedp/chromedp/compare/v0.11.0...v0.11.1) --- updated-dependencies: - dependency-name: github.com/chromedp/chromedp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index adbfc34b3..d272b30c5 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/alicebob/miniredis/v2 v2.33.0 github.com/avast/retry-go/v4 v4.6.0 github.com/cbroglie/mustache v1.4.0 - github.com/chromedp/chromedp v0.11.0 + github.com/chromedp/chromedp v0.11.1 github.com/dlclark/regexp2 v1.11.4 github.com/go-redis/redismock/v9 v9.2.0 github.com/goodsign/monday v1.0.2 @@ -83,8 +83,8 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df // indirect - github.com/chromedp/sysutil v1.0.0 // indirect + github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect + github.com/chromedp/sysutil v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect diff --git a/go.sum b/go.sum index a148a0413..238882d53 100644 --- a/go.sum +++ b/go.sum @@ -74,12 +74,12 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df h1:cbtSn19AtqQha1cxmP2Qvgd3fFMz51AeAEKLJMyEUhc= -github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/chromedp v0.11.0 h1:1PT6O4g39sBAFjlljIHTpxmCSk8meeYL6+R+oXH4bWA= -github.com/chromedp/chromedp v0.11.0/go.mod h1:jsD7OHrX0Qmskqb5Y4fn4jHnqquqW22rkMFgKbECsqg= -github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= -github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU= +github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM= +github.com/chromedp/chromedp v0.11.1 h1:Spca8egFqUlv+JDW+yIs+ijlHlJDPufgrfXPwtq6NMs= +github.com/chromedp/chromedp v0.11.1/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 3ac99ee017fe1e9eac1ed5778a0d5eba3f880e35 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Wed, 23 Oct 2024 15:12:51 +0200 Subject: [PATCH 63/72] Fix duplicate discovery results (#3515) * rewrite wildcard query and VP search using group by and sub select --- discovery/store.go | 67 +++++++++++++++++++++----- discovery/store_test.go | 7 +++ e2e-tests/discovery/run-test.sh | 2 +- e2e-tests/oauth-flow/rfc021/do-test.sh | 10 ++++ 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/discovery/store.go b/discovery/store.go index 2f6ddd907..5dfba8298 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -26,6 +26,7 @@ import ( "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/vcr/credential/store" "slices" + "strconv" "strings" "time" @@ -267,25 +268,19 @@ func (s *sqlStore) get(serviceID string, startAfter int) (map[string]vc.Verifiab func (s *sqlStore) search(serviceID string, query map[string]string, allowUnvalidated bool) ([]vc.VerifiablePresentation, error) { // first only select columns also used in group by clause // if the query is empty, there's no need to do a join - stmt := s.db.Model(&presentationRecord{}). + stmt := s.db.Model(&presentationRecord{}).Select("discovery_presentation.id"). Where("service_id = ?", serviceID) if !allowUnvalidated { stmt = stmt.Where("validated != 0") } - // remove wildcards to prevent unneeded join on credential - filteredQuery := make(map[string]string) - for k, v := range query { - if strings.TrimSpace(v) != "*" { - filteredQuery[k] = v - } - } - if len(filteredQuery) > 0 { - stmt = stmt.Joins("inner join discovery_credential ON discovery_credential.presentation_id = discovery_presentation.id") - stmt = store.CredentialStore{}.BuildSearchStatement(stmt, "discovery_credential.credential_id", filteredQuery) + if len(query) > 0 { + stmt = applyQuery(stmt, query) } + stmt = stmt.Group("discovery_presentation.id") var matches []presentationRecord - if err := stmt.Preload("Credentials").Preload("Credentials.Credential").Find(&matches).Error; err != nil { + main := s.db.Preload("Credentials").Preload("Credentials.Credential").Model(&presentationRecord{}).Where("id in (?)", stmt) + if err := main.Find(&matches).Error; err != nil { return nil, err } var results []vc.VerifiablePresentation @@ -302,6 +297,52 @@ func (s *sqlStore) search(serviceID string, query map[string]string, allowUnvali return results, nil } +// applyQuery is like vcr/credential/store/sql.go#BuildSearchStatement but for searching VPs a group by is needed which also requires a sub query +// at that point a generic search statement is not maintainable +func applyQuery(stmt *gorm.DB, query map[string]string) *gorm.DB { + propertyColumns := map[string]string{ + "id": "credential.id", + "issuer": "credential.issuer", + "type": "credential.type", + "credentialSubject.id": "credential.subject_id", + } + + stmt = stmt.Joins("inner join discovery_credential ON discovery_credential.presentation_id = discovery_presentation.id") + stmt = stmt.Joins("inner join credential ON credential.id = discovery_credential.credential_id") + numProps := 0 + for jsonPath, value := range query { + // sort out wildcard mode: prefix and postfix asterisks (*) are replaced with %, which then is used in a LIKE query. + // an asterisk is translated to IS NOT NULL + // Otherwise, exact match (=) is used. + var op = "= ?" + if strings.TrimSpace(value) == "*" { + op = "is not null" + value = "" + } else { + if strings.HasPrefix(value, "*") { + value = "%" + value[1:] + op = "LIKE ?" + } + // and or + if strings.HasSuffix(value, "*") { + value = value[:len(value)-1] + "%" + op = "LIKE ?" + } + } + if column := propertyColumns[jsonPath]; column != "" { + stmt = stmt.Where(column+" "+op, value) + } else { + // This property is not present as column, but indexed as key-value property. + // Multiple (inner) joins to filter on a dynamic number of properties to filter on is not pretty, but it works + alias := "p" + strconv.Itoa(numProps) + numProps++ + // for an IS NOT NULL query, the value is ignored + stmt = stmt.Joins("inner join credential_prop "+alias+" ON "+alias+".credential_id = credential.id AND "+alias+".path = ? AND "+alias+".value "+op, jsonPath, value) + } + } + return stmt +} + // incrementTimestamp increments the last_timestamp of the given service. USed by server. func (s *sqlStore) incrementTimestamp(tx *gorm.DB, serviceID string, seed string) (*int, error) { service, err := s.findAndLockService(tx, serviceID) @@ -406,7 +447,7 @@ func (s *sqlStore) allPresentations(validated bool) ([]presentationRecord, error func (s *sqlStore) updateValidated(records []presentationRecord) error { return s.db.Transaction(func(tx *gorm.DB) error { for _, record := range records { - if err := tx.Model(&presentationRecord{}).Where("id = ?", record.ID).Update("validated", true).Error; err != nil { + if err := tx.Model(&presentationRecord{}).Where("id = ?", record.ID).Update("validated", SQLBool(true)).Error; err != nil { return err } } diff --git a/discovery/store_test.go b/discovery/store_test.go index 7df338a6e..d1b73ef23 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -272,6 +272,13 @@ func Test_sqlStore_search(t *testing.T) { }, true) require.NoError(t, err) require.Len(t, actualVPs, 0) + + t.Run("wildcard", func(t *testing.T) { + actualVPs, err = c.search(testServiceID, map[string]string{"credentialSubject.person.noName": "*"}, true) + require.NoError(t, err) + require.Len(t, actualVPs, 0) + }) + }) } diff --git a/e2e-tests/discovery/run-test.sh b/e2e-tests/discovery/run-test.sh index 5c4cd38d2..224fee67c 100755 --- a/e2e-tests/discovery/run-test.sh +++ b/e2e-tests/discovery/run-test.sh @@ -62,7 +62,7 @@ sleep 2 echo "---------------------------------------" echo "Searching for care organization registration on Discovery Server..." echo "---------------------------------------" -RESPONSE=$(curl -s --insecure "http://localhost:18081/internal/discovery/v1/dev:eOverdracht2023?credentialSubject.organization.name=Care*") +RESPONSE=$(curl -s --insecure "http://localhost:18081/internal/discovery/v1/dev:eOverdracht2023?credentialSubject.organization.name=Care*&credentialSubject.organization.city=*") NUM_ITEMS=$(echo $RESPONSE | jq length) if [ $NUM_ITEMS -eq 1 ]; then echo "Registration found" diff --git a/e2e-tests/oauth-flow/rfc021/do-test.sh b/e2e-tests/oauth-flow/rfc021/do-test.sh index b6b42ab03..f7aebaabe 100755 --- a/e2e-tests/oauth-flow/rfc021/do-test.sh +++ b/e2e-tests/oauth-flow/rfc021/do-test.sh @@ -104,6 +104,16 @@ else exitWithDockerLogs 1 fi +# Search for registration using wildcards, results in complicated DB query +RESPONSE=$(curl -s --insecure http://localhost:28081/internal/discovery/v1/e2e-test?credentialSubject.organization.name=*) +NUM_ITEMS=$(echo $RESPONSE | jq length) +if [ $NUM_ITEMS -eq 1 ]; then + echo "Registration found" +else + echo "FAILED: Could not find registration" 1>&2 + exitWithDockerLogs 1 +fi + echo "---------------------------------------" echo "Perform OAuth 2.0 rfc021 flow..." echo "---------------------------------------" From 1e5c67271b83eb0ee6eb1f8312dee89be1e4d933 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:11:08 +0200 Subject: [PATCH 64/72] Require SQL connection string in strictmode (#3517) * require SQL connection string in strictmode * fix e2e-tests * fix tests --- README.rst | 4 ++-- crypto/cmd/cmd_test.go | 3 +-- docs/pages/deployment/configuration.rst | 4 ++-- docs/pages/deployment/storage.rst | 11 ++++++++--- e2e-tests/nuts-network/direct-wan/node-A/nuts.yaml | 3 +++ e2e-tests/nuts-network/direct-wan/node-B/nuts.yaml | 3 +++ .../private-transactions/node-A/nuts.yaml | 3 +++ .../private-transactions/node-B/nuts.yaml | 3 +++ .../ssl-offloading/haproxy/node-A/nuts.yaml | 3 +++ .../ssl-offloading/haproxy/node-B/nuts.yaml | 3 +++ .../ssl-offloading/nginx/node-A/nuts.yaml | 3 +++ .../ssl-offloading/nginx/node-B/nuts.yaml | 3 +++ .../nuts-network/ssl-pass-through/node-A/nuts.yaml | 3 +++ .../nuts-network/ssl-pass-through/node-B/nuts.yaml | 3 +++ .../openid4vci/issuer-initiated/node-A/nuts.yaml | 3 +++ .../openid4vci/issuer-initiated/node-B/nuts.yaml | 3 +++ .../openid4vci/network-issuance/node-A/nuts.yaml | 3 +++ .../openid4vci/network-issuance/node-B/nuts.yaml | 3 +++ e2e-tests/ops/create-subject/docker-compose.yml | 4 ---- e2e-tests/ops/key-rotation/node-A/nuts.yaml | 3 +++ e2e-tests/ops/key-rotation/node-B/nuts.yaml | 3 +++ e2e-tests/storage/redis/node-A/nuts.yaml | 2 ++ e2e-tests/storage/redis/node-B/nuts.yaml | 2 ++ e2e-tests/storage/vault/nuts.yaml | 3 +++ main_test.go | 12 ++++++++++++ storage/engine.go | 8 ++++++-- storage/engine_test.go | 8 +++++++- 27 files changed, 93 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 20748003e..14908033a 100644 --- a/README.rst +++ b/README.rst @@ -301,8 +301,8 @@ Several of the server options above allow the node to be configured in a way tha The node can be configured to run in strict mode (default) to prevent any insecure configurations. Below is a summary of the impact ``strictmode=true`` has on the node and its configuration. -Save storage of any private key material requires some serious consideration. -For this reason the ``crypto.storage`` backend must explicitly be set. +Save storage of any private key material and data requires some serious consideration. +For this reason the ``crypto.storage`` backend and the ``storage.sql.connection`` connection string must explicitly be set. Private transactions can only be exchanged over authenticated nodes. Therefore is requires TLS to be configured through ``tls.{certfile,certkeyfile,truststore}``. diff --git a/crypto/cmd/cmd_test.go b/crypto/cmd/cmd_test.go index 4cd82aba0..34f9a2bc9 100644 --- a/crypto/cmd/cmd_test.go +++ b/crypto/cmd/cmd_test.go @@ -92,11 +92,10 @@ func Test_fs2VaultCommand(t *testing.T) { // Configure target t.Setenv("NUTS_CRYPTO_STORAGE", "vaultkv") t.Setenv("NUTS_CRYPTO_VAULT_ADDRESS", s.URL) + t.Setenv("NUTS_STRICTMODE", "false") testDirectory := testIo.TestDirectory(t) setupFSStoreData(t, testDirectory) - // default datadir is unavailable causing sqlite to fail - t.Setenv("NUTS_DATADIR", testDirectory) outBuf := new(bytes.Buffer) cryptoCmd := ServerCmd() diff --git a/docs/pages/deployment/configuration.rst b/docs/pages/deployment/configuration.rst index 88f691994..4385fcb20 100644 --- a/docs/pages/deployment/configuration.rst +++ b/docs/pages/deployment/configuration.rst @@ -77,8 +77,8 @@ Several of the server options above allow the node to be configured in a way tha The node can be configured to run in strict mode (default) to prevent any insecure configurations. Below is a summary of the impact ``strictmode=true`` has on the node and its configuration. -Save storage of any private key material requires some serious consideration. -For this reason the ``crypto.storage`` backend must explicitly be set. +Save storage of any private key material and data requires some serious consideration. +For this reason the ``crypto.storage`` backend and the ``storage.sql.connection`` connection string must explicitly be set. Private transactions can only be exchanged over authenticated nodes. Therefore is requires TLS to be configured through ``tls.{certfile,certkeyfile,truststore}``. diff --git a/docs/pages/deployment/storage.rst b/docs/pages/deployment/storage.rst index 10f0181e4..413a48f59 100644 --- a/docs/pages/deployment/storage.rst +++ b/docs/pages/deployment/storage.rst @@ -21,9 +21,9 @@ Also remember to test your backup and restore procedure. SQL database ************ -By default, storage SQLite will be used in a file called ``sqlite.db`` in the configured data directory. -This can be overridden by configuring a connection string in ``storage.sql.connection``. -Other supported SQL databases are Postgres, MySQL, Microsoft SQL Server and Microsoft Azure SQL Server. +Currently supported SQL databases are Postgres, MySQL, Microsoft SQL Server, Microsoft Azure SQL Server, and SQLite. +The database of your preference can be set by configuring a connection string in ``storage.sql.connection``. +Only in non-strictmode, if no connection string is set this will default to SQLite in a file called ``sqlite.db`` in the configured data directory. Connection strings must be in the following format: @@ -39,6 +39,11 @@ Refer to the documentation of the driver for the database you are using for the - Azure SQL Server: `github.com/microsoft/go-mssqldb `_ (e.g. ``azuresql://server=awesome-server;port=1433;database=awesome-db;fedauth=ActiveDirectoryDefault;``) - SQLite (e.g. ``sqlite:file:/some/path/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)``) +.. warning:: + + Usage of SQLite is not recommended for production environments. + Connections to a SQLite DB are restricted to 1, which will lead to severe performance reduction. + Private Keys ************ diff --git a/e2e-tests/nuts-network/direct-wan/node-A/nuts.yaml b/e2e-tests/nuts-network/direct-wan/node-A/nuts.yaml index 27cd52bdb..2e6cf08f2 100644 --- a/e2e-tests/nuts-network/direct-wan/node-A/nuts.yaml +++ b/e2e-tests/nuts-network/direct-wan/node-A/nuts.yaml @@ -17,3 +17,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" \ No newline at end of file diff --git a/e2e-tests/nuts-network/direct-wan/node-B/nuts.yaml b/e2e-tests/nuts-network/direct-wan/node-B/nuts.yaml index 17c5bfdc6..97ad4019c 100644 --- a/e2e-tests/nuts-network/direct-wan/node-B/nuts.yaml +++ b/e2e-tests/nuts-network/direct-wan/node-B/nuts.yaml @@ -18,3 +18,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/private-transactions/node-A/nuts.yaml b/e2e-tests/nuts-network/private-transactions/node-A/nuts.yaml index aaa9186c0..c4d541af9 100644 --- a/e2e-tests/nuts-network/private-transactions/node-A/nuts.yaml +++ b/e2e-tests/nuts-network/private-transactions/node-A/nuts.yaml @@ -26,3 +26,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 500 +storage: + sql: + connection: "sqlite:file:/opt/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/private-transactions/node-B/nuts.yaml b/e2e-tests/nuts-network/private-transactions/node-B/nuts.yaml index 04ab35b07..71573460a 100644 --- a/e2e-tests/nuts-network/private-transactions/node-B/nuts.yaml +++ b/e2e-tests/nuts-network/private-transactions/node-B/nuts.yaml @@ -26,3 +26,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 450 +storage: + sql: + connection: "sqlite:file:/opt/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/ssl-offloading/haproxy/node-A/nuts.yaml b/e2e-tests/nuts-network/ssl-offloading/haproxy/node-A/nuts.yaml index 6d34ac964..945bf02d8 100644 --- a/e2e-tests/nuts-network/ssl-offloading/haproxy/node-A/nuts.yaml +++ b/e2e-tests/nuts-network/ssl-offloading/haproxy/node-A/nuts.yaml @@ -21,3 +21,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 250 +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/ssl-offloading/haproxy/node-B/nuts.yaml b/e2e-tests/nuts-network/ssl-offloading/haproxy/node-B/nuts.yaml index 084050510..e70a52860 100644 --- a/e2e-tests/nuts-network/ssl-offloading/haproxy/node-B/nuts.yaml +++ b/e2e-tests/nuts-network/ssl-offloading/haproxy/node-B/nuts.yaml @@ -22,3 +22,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 250 +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/ssl-offloading/nginx/node-A/nuts.yaml b/e2e-tests/nuts-network/ssl-offloading/nginx/node-A/nuts.yaml index 6d34ac964..945bf02d8 100644 --- a/e2e-tests/nuts-network/ssl-offloading/nginx/node-A/nuts.yaml +++ b/e2e-tests/nuts-network/ssl-offloading/nginx/node-A/nuts.yaml @@ -21,3 +21,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 250 +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/ssl-offloading/nginx/node-B/nuts.yaml b/e2e-tests/nuts-network/ssl-offloading/nginx/node-B/nuts.yaml index d0644ede0..eb04c225c 100644 --- a/e2e-tests/nuts-network/ssl-offloading/nginx/node-B/nuts.yaml +++ b/e2e-tests/nuts-network/ssl-offloading/nginx/node-B/nuts.yaml @@ -22,3 +22,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 250 +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/ssl-pass-through/node-A/nuts.yaml b/e2e-tests/nuts-network/ssl-pass-through/node-A/nuts.yaml index 22143a2bf..ad5b3d2d5 100644 --- a/e2e-tests/nuts-network/ssl-pass-through/node-A/nuts.yaml +++ b/e2e-tests/nuts-network/ssl-pass-through/node-A/nuts.yaml @@ -18,3 +18,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/nuts-network/ssl-pass-through/node-B/nuts.yaml b/e2e-tests/nuts-network/ssl-pass-through/node-B/nuts.yaml index 208c79958..78482d93f 100644 --- a/e2e-tests/nuts-network/ssl-pass-through/node-B/nuts.yaml +++ b/e2e-tests/nuts-network/ssl-pass-through/node-B/nuts.yaml @@ -19,3 +19,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/openid4vci/issuer-initiated/node-A/nuts.yaml b/e2e-tests/openid4vci/issuer-initiated/node-A/nuts.yaml index b18a53712..467ba49a0 100644 --- a/e2e-tests/openid4vci/issuer-initiated/node-A/nuts.yaml +++ b/e2e-tests/openid4vci/issuer-initiated/node-A/nuts.yaml @@ -24,3 +24,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 500 +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/openid4vci/issuer-initiated/node-B/nuts.yaml b/e2e-tests/openid4vci/issuer-initiated/node-B/nuts.yaml index a1aadf9a4..b0acca980 100644 --- a/e2e-tests/openid4vci/issuer-initiated/node-B/nuts.yaml +++ b/e2e-tests/openid4vci/issuer-initiated/node-B/nuts.yaml @@ -25,3 +25,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 450 +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/openid4vci/network-issuance/node-A/nuts.yaml b/e2e-tests/openid4vci/network-issuance/node-A/nuts.yaml index c032185df..65840f71c 100644 --- a/e2e-tests/openid4vci/network-issuance/node-A/nuts.yaml +++ b/e2e-tests/openid4vci/network-issuance/node-A/nuts.yaml @@ -28,3 +28,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 500 +storage: + sql: + connection: "sqlite:file:/opt/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/openid4vci/network-issuance/node-B/nuts.yaml b/e2e-tests/openid4vci/network-issuance/node-B/nuts.yaml index b64d43eeb..42a4e5b9b 100644 --- a/e2e-tests/openid4vci/network-issuance/node-B/nuts.yaml +++ b/e2e-tests/openid4vci/network-issuance/node-B/nuts.yaml @@ -29,3 +29,6 @@ network: grpcaddr: :5555 v2: gossipinterval: 450 +storage: + sql: + connection: "sqlite:file:/opt/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/ops/create-subject/docker-compose.yml b/e2e-tests/ops/create-subject/docker-compose.yml index 28d1a2ee4..0e105c0af 100644 --- a/e2e-tests/ops/create-subject/docker-compose.yml +++ b/e2e-tests/ops/create-subject/docker-compose.yml @@ -3,9 +3,5 @@ services: image: "${IMAGE_NODE_A:-nutsfoundation/nuts-node:master}" ports: - "18081:8081" - environment: - NUTS_CONFIGFILE: /opt/nuts/nuts.yaml - volumes: - - "./nuts.yaml:/opt/nuts/nuts.yaml:ro" healthcheck: interval: 1s # Make test run quicker by checking health status more often \ No newline at end of file diff --git a/e2e-tests/ops/key-rotation/node-A/nuts.yaml b/e2e-tests/ops/key-rotation/node-A/nuts.yaml index 9e1ac04ca..1b2824f65 100644 --- a/e2e-tests/ops/key-rotation/node-A/nuts.yaml +++ b/e2e-tests/ops/key-rotation/node-A/nuts.yaml @@ -15,3 +15,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/ops/key-rotation/node-B/nuts.yaml b/e2e-tests/ops/key-rotation/node-B/nuts.yaml index 11b74208a..158918d73 100644 --- a/e2e-tests/ops/key-rotation/node-B/nuts.yaml +++ b/e2e-tests/ops/key-rotation/node-B/nuts.yaml @@ -16,3 +16,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/storage/redis/node-A/nuts.yaml b/e2e-tests/storage/redis/node-A/nuts.yaml index c782f6aa9..92942772e 100644 --- a/e2e-tests/storage/redis/node-A/nuts.yaml +++ b/e2e-tests/storage/redis/node-A/nuts.yaml @@ -21,3 +21,5 @@ storage: redis: address: redis:6379 database: nodeA + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/storage/redis/node-B/nuts.yaml b/e2e-tests/storage/redis/node-B/nuts.yaml index 69f9ee791..b17a2e4e9 100644 --- a/e2e-tests/storage/redis/node-B/nuts.yaml +++ b/e2e-tests/storage/redis/node-B/nuts.yaml @@ -22,3 +22,5 @@ storage: redis: address: redis:6379 database: nodeB + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/e2e-tests/storage/vault/nuts.yaml b/e2e-tests/storage/vault/nuts.yaml index b8e877c3d..7a014549d 100644 --- a/e2e-tests/storage/vault/nuts.yaml +++ b/e2e-tests/storage/vault/nuts.yaml @@ -19,3 +19,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem +storage: + sql: + connection: "sqlite:file:/nuts/data/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)" diff --git a/main_test.go b/main_test.go index 6c86314d8..45df58769 100644 --- a/main_test.go +++ b/main_test.go @@ -32,6 +32,7 @@ import ( "github.com/nuts-foundation/nuts-node/events" httpEngine "github.com/nuts-foundation/nuts-node/http" "github.com/nuts-foundation/nuts-node/network" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/test" "github.com/nuts-foundation/nuts-node/test/pki" v1 "github.com/nuts-foundation/nuts-node/vdr/api/v1" @@ -146,6 +147,17 @@ func startServer(testDirectory string, exitCallback func(), serverConfig core.Se if err != nil { panic(err) } + if serverConfig.Strictmode { + type modCfg struct { + Storage storage.Config `koanf:"storage"` + } + storageConfig := modCfg{Storage: storage.DefaultConfig()} + storageConfig.Storage.SQL.ConnectionString = fmt.Sprintf("sqlite:file:%s/sqlite.db?_pragma=foreign_keys(1)&journal_mode(WAL)", testDirectory) + err = koanfInstance.Load(structs.ProviderWithDelim(storageConfig, "koanf", "."), nil) + if err != nil { + panic(err) + } + } bytes, err := koanfInstance.Marshal(yamlParser) if err != nil { diff --git a/storage/engine.go b/storage/engine.go index 3619c7115..6826d0a3b 100644 --- a/storage/engine.go +++ b/storage/engine.go @@ -172,7 +172,7 @@ func (e *engine) Configure(config core.ServerConfig) error { e.databases = append(e.databases, bboltDB) // SQL storage - if err := e.initSQLDatabase(); err != nil { + if err := e.initSQLDatabase(config.Strictmode); err != nil { return fmt.Errorf("failed to initialize SQL database: %w", err) } @@ -212,9 +212,13 @@ func (e *engine) GetSQLDatabase() *gorm.DB { // initSQLDatabase initializes the SQL database connection. // If the connection string is not configured, it defaults to a SQLite database, stored in the node's data directory. -func (e *engine) initSQLDatabase() error { +func (e *engine) initSQLDatabase(strictmode bool) error { connectionString := e.config.SQL.ConnectionString if len(connectionString) == 0 { + if strictmode { + return errors.New("no database configured: storage.sql.connection must be set in strictmode") + } + // non-strictmode uses SQLite as default connectionString = sqliteConnectionString(e.datadir) } diff --git a/storage/engine_test.go b/storage/engine_test.go index f31593a76..0e6b68aff 100644 --- a/storage/engine_test.go +++ b/storage/engine_test.go @@ -120,9 +120,15 @@ func Test_engine_sqlDatabase(t *testing.T) { require.NoError(t, os.Remove(dataDir)) e := New() e.(*engine).datadir = dataDir - err := e.(*engine).initSQLDatabase() + err := e.(*engine).initSQLDatabase(false) assert.ErrorContains(t, err, "unable to open database file") }) + t.Run("no DB configured in strictmode", func(t *testing.T) { + e := New() + e.(*engine).datadir = io.TestDirectory(t) + err := e.(*engine).initSQLDatabase(true) + assert.ErrorContains(t, err, "no database configured: storage.sql.connection must be set in strictmode") + }) t.Run("sqlite is restricted to 1 connection", func(t *testing.T) { e := New() require.NoError(t, e.Configure(core.ServerConfig{Datadir: t.TempDir()})) From 9296c03334cf4dc23493efa3885da748b76ffd97 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:04:38 +0200 Subject: [PATCH 65/72] status codes for discovery client (#3513) * status codes for discovery client * pr feedback * pr feedback --- auth/api/iam/generated.go | 4 +- discovery/api/server/client/http.go | 5 +- discovery/api/server/client/http_test.go | 16 ++--- discovery/api/v1/api.go | 18 +++-- discovery/api/v1/api_test.go | 17 ++++- discovery/api/v1/generated.go | 66 ++--------------- discovery/api/v1/types.go | 4 +- discovery/client.go | 88 +++++++++++------------ discovery/client_test.go | 36 +++++----- discovery/definition.go | 2 +- discovery/interface.go | 12 +++- discovery/module.go | 20 +++--- discovery/module_test.go | 37 ++++++---- docs/_static/discovery/v1.yaml | 36 +++++----- e2e-tests/browser/client/iam/generated.go | 4 +- vdr/api/v2/generated.go | 1 + 16 files changed, 171 insertions(+), 195 deletions(-) diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index 60ea89cb5..c8135c523 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -84,7 +84,7 @@ type ExtendedTokenIntrospectionResponse struct { // Aud RFC7662 - Service-specific string identifier or list of string identifiers representing the intended audience for this token, as defined in JWT [RFC7519]. Aud *string `json:"aud,omitempty"` - // ClientId The client (DID) the access token was issued to + // ClientId The client identity the access token was issued to. Since the Verifiable Presentation is used to grant access, the client_id reflects the client_id in the access token request. ClientId *string `json:"client_id,omitempty"` // Cnf The 'confirmation' claim is used in JWTs to proof the possession of a key. @@ -96,7 +96,7 @@ type ExtendedTokenIntrospectionResponse struct { // Iat Issuance time in seconds since UNIX epoch Iat *int `json:"iat,omitempty"` - // Iss Contains the DID of the authorizer. Should be equal to 'sub' + // Iss Issuer URL of the authorizer. Iss *string `json:"iss,omitempty"` // PresentationDefinitions Presentation Definitions, as described in Presentation Exchange specification, fulfilled to obtain the access token diff --git a/discovery/api/server/client/http.go b/discovery/api/server/client/http.go index 6623b642a..b819be4c9 100644 --- a/discovery/api/server/client/http.go +++ b/discovery/api/server/client/http.go @@ -21,7 +21,6 @@ package client import ( "bytes" "context" - "crypto/tls" "encoding/json" "fmt" "github.com/nuts-foundation/go-did/vc" @@ -35,9 +34,9 @@ import ( ) // New creates a new DefaultHTTPClient. -func New(strictMode bool, timeout time.Duration, tlsConfig *tls.Config) *DefaultHTTPClient { +func New(timeout time.Duration) *DefaultHTTPClient { return &DefaultHTTPClient{ - client: client.NewWithTLSConfig(timeout, tlsConfig), + client: client.New(timeout), } } diff --git a/discovery/api/server/client/http_test.go b/discovery/api/server/client/http_test.go index bfe0717ce..ac40dd855 100644 --- a/discovery/api/server/client/http_test.go +++ b/discovery/api/server/client/http_test.go @@ -40,7 +40,7 @@ func TestHTTPInvoker_Register(t *testing.T) { t.Run("ok", func(t *testing.T) { handler := &testHTTP.Handler{StatusCode: http.StatusCreated} server := httptest.NewServer(handler) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) err := client.Register(context.Background(), server.URL, vp) @@ -51,7 +51,7 @@ func TestHTTPInvoker_Register(t *testing.T) { }) t.Run("non-ok with problem details", func(t *testing.T) { server := httptest.NewServer(&testHTTP.Handler{StatusCode: http.StatusBadRequest, ResponseData: `{"title":"missing credentials", "status":400, "detail":"could not resolve DID"}`}) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) err := client.Register(context.Background(), server.URL, vp) @@ -61,7 +61,7 @@ func TestHTTPInvoker_Register(t *testing.T) { }) t.Run("non-ok other", func(t *testing.T) { server := httptest.NewServer(&testHTTP.Handler{StatusCode: http.StatusNotFound, ResponseData: `not found`}) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) err := client.Register(context.Background(), server.URL, vp) @@ -84,7 +84,7 @@ func TestHTTPInvoker_Get(t *testing.T) { "timestamp": 1, } server := httptest.NewServer(handler) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) presentations, seed, timestamp, err := client.Get(context.Background(), server.URL, 0) @@ -102,7 +102,7 @@ func TestHTTPInvoker_Get(t *testing.T) { "timestamp": 1, } server := httptest.NewServer(handler) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) presentations, _, timestamp, err := client.Get(context.Background(), server.URL, 1) @@ -120,7 +120,7 @@ func TestHTTPInvoker_Get(t *testing.T) { writer.Write([]byte("{}")) } server := httptest.NewServer(http.HandlerFunc(handler)) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) _, _, _, err := client.Get(context.Background(), server.URL, 0) @@ -130,7 +130,7 @@ func TestHTTPInvoker_Get(t *testing.T) { t.Run("server returns invalid status code", func(t *testing.T) { handler := &testHTTP.Handler{StatusCode: http.StatusInternalServerError, ResponseData: `{"title":"internal server error", "status":500, "detail":"db not found"}`} server := httptest.NewServer(handler) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) _, _, _, err := client.Get(context.Background(), server.URL, 0) @@ -142,7 +142,7 @@ func TestHTTPInvoker_Get(t *testing.T) { handler := &testHTTP.Handler{StatusCode: http.StatusOK} handler.ResponseData = "not json" server := httptest.NewServer(handler) - client := New(false, time.Minute, server.TLS) + client := New(time.Minute) _, _, _, err := client.Get(context.Background(), server.URL, 0) diff --git a/discovery/api/v1/api.go b/discovery/api/v1/api.go index 6632c4ca5..b05a56ee5 100644 --- a/discovery/api/v1/api.go +++ b/discovery/api/v1/api.go @@ -121,6 +121,9 @@ func (w *Wrapper) ActivateServiceForSubject(ctx context.Context, request Activat func (w *Wrapper) DeactivateServiceForSubject(ctx context.Context, request DeactivateServiceForSubjectRequestObject) (DeactivateServiceForSubjectResponseObject, error) { err := w.Client.DeactivateServiceForSubject(ctx, request.ServiceID, request.SubjectID) if err != nil { + if errors.Is(err, discovery.ErrPresentationRegistrationFailed) { + return DeactivateServiceForSubject202JSONResponse{Reason: err.Error()}, nil + } return nil, err } return DeactivateServiceForSubject200Response{}, nil @@ -132,19 +135,24 @@ func (w *Wrapper) GetServices(_ context.Context, _ GetServicesRequestObject) (Ge } func (w *Wrapper) GetServiceActivation(ctx context.Context, request GetServiceActivationRequestObject) (GetServiceActivationResponseObject, error) { - response := GetServiceActivation200JSONResponse{ - Status: ServiceStatusActive, - } + response := GetServiceActivation200JSONResponse{} activated, presentations, err := w.Client.GetServiceActivation(ctx, request.ServiceID, request.SubjectID) if err != nil { if !errors.As(err, &discovery.RegistrationRefreshError{}) { return nil, err } - response.Status = ServiceStatusError + response.Status = to.Ptr(ServiceStatusError) response.Error = to.Ptr(err.Error()) } response.Activated = activated - response.Vp = &presentations + if activated && response.Status == nil { + // only set if not already set to ServiceStatusError + response.Status = to.Ptr(ServiceStatusActive) + } + if presentations != nil { + // if presentations is nil this would add `"vp":null` to the response + response.Vp = &presentations + } return response, nil } diff --git a/discovery/api/v1/api_test.go b/discovery/api/v1/api_test.go index 578a47ddf..3a831ac36 100644 --- a/discovery/api/v1/api_test.go +++ b/discovery/api/v1/api_test.go @@ -108,6 +108,19 @@ func TestWrapper_DeactivateServiceForSubject(t *testing.T) { assert.NoError(t, err) assert.IsType(t, DeactivateServiceForSubject200Response{}, response) }) + t.Run("server error", func(t *testing.T) { + test := newMockContext(t) + expectedErr := errors.Join(discovery.ErrPresentationRegistrationFailed, errors.New("custom error")) + test.client.EXPECT().DeactivateServiceForSubject(gomock.Any(), serviceID, subjectID).Return(expectedErr) + + response, err := test.wrapper.DeactivateServiceForSubject(nil, DeactivateServiceForSubjectRequestObject{ + ServiceID: serviceID, + SubjectID: subjectID, + }) + + assert.NoError(t, err) + assert.IsType(t, DeactivateServiceForSubject202JSONResponse{Reason: expectedErr.Error()}, response) + }) t.Run("error", func(t *testing.T) { test := newMockContext(t) test.client.EXPECT().DeactivateServiceForSubject(gomock.Any(), serviceID, subjectID).Return(errors.New("foo")) @@ -199,7 +212,7 @@ func TestWrapper_GetServiceActivation(t *testing.T) { assert.NoError(t, err) require.IsType(t, GetServiceActivation200JSONResponse{}, response) assert.True(t, response.(GetServiceActivation200JSONResponse).Activated) - assert.Equal(t, ServiceStatusActive, string(response.(GetServiceActivation200JSONResponse).Status)) + assert.Equal(t, ServiceStatusActive, *response.(GetServiceActivation200JSONResponse).Status) assert.Nil(t, response.(GetServiceActivation200JSONResponse).Error) assert.Empty(t, response.(GetServiceActivation200JSONResponse).Vp) }) @@ -215,7 +228,7 @@ func TestWrapper_GetServiceActivation(t *testing.T) { assert.NoError(t, err) require.IsType(t, GetServiceActivation200JSONResponse{}, response) assert.True(t, response.(GetServiceActivation200JSONResponse).Activated) - assert.Equal(t, ServiceStatusError, string(response.(GetServiceActivation200JSONResponse).Status)) + assert.Equal(t, ServiceStatusError, *response.(GetServiceActivation200JSONResponse).Status) assert.NotNil(t, response.(GetServiceActivation200JSONResponse).Error) assert.Empty(t, response.(GetServiceActivation200JSONResponse).Vp) }) diff --git a/discovery/api/v1/generated.go b/discovery/api/v1/generated.go index 11e76317d..69cde00bd 100644 --- a/discovery/api/v1/generated.go +++ b/discovery/api/v1/generated.go @@ -61,13 +61,13 @@ type ServerInterface interface { // Searches for presentations registered on the Discovery Service. // (GET /internal/discovery/v1/{serviceID}) SearchPresentations(ctx echo.Context, serviceID string, params SearchPresentationsParams) error - // Client API to deactivate the given subject from the Discovery Service. + // Remove a subject from the Discovery Service. // (DELETE /internal/discovery/v1/{serviceID}/{subjectID}) DeactivateServiceForSubject(ctx echo.Context, serviceID string, subjectID string) error // Retrieves the activation status of a subject on a Discovery Service. // (GET /internal/discovery/v1/{serviceID}/{subjectID}) GetServiceActivation(ctx echo.Context, serviceID string, subjectID string) error - // Client API to activate a subject on the specified Discovery Service. + // Activate a Discovery Service for a subject. // (POST /internal/discovery/v1/{serviceID}/{subjectID}) ActivateServiceForSubject(ctx echo.Context, serviceID string, subjectID string) error } @@ -325,24 +325,6 @@ func (response DeactivateServiceForSubject202JSONResponse) VisitDeactivateServic return json.NewEncoder(w).Encode(response) } -type DeactivateServiceForSubject400ApplicationProblemPlusJSONResponse struct { - // Detail A human-readable explanation specific to this occurrence of the problem. - Detail string `json:"detail"` - - // Status HTTP statuscode - Status float32 `json:"status"` - - // Title A short, human-readable summary of the problem type. - Title string `json:"title"` -} - -func (response DeactivateServiceForSubject400ApplicationProblemPlusJSONResponse) VisitDeactivateServiceForSubjectResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/problem+json") - w.WriteHeader(400) - - return json.NewEncoder(w).Encode(response) -} - type DeactivateServiceForSubjectdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. @@ -381,10 +363,10 @@ type GetServiceActivation200JSONResponse struct { Error *string `json:"error,omitempty"` // Status Status of the activation. "active" or "error". - Status GetServiceActivation200JSONResponseStatus `json:"status"` + Status *GetServiceActivation200JSONResponseStatus `json:"status,omitempty"` // Vp List of VPs on the Discovery Service for the subject. One per DID method registered on the Service. - // The list can be empty even if activated==true if none of the DIDs of a subject is actually registered on the Discovery Service. + // The list is empty when status is "error". Vp *[]VerifiablePresentation `json:"vp,omitempty"` } @@ -434,42 +416,6 @@ func (response ActivateServiceForSubject200Response) VisitActivateServiceForSubj return nil } -type ActivateServiceForSubject400ApplicationProblemPlusJSONResponse struct { - // Detail A human-readable explanation specific to this occurrence of the problem. - Detail string `json:"detail"` - - // Status HTTP statuscode - Status float32 `json:"status"` - - // Title A short, human-readable summary of the problem type. - Title string `json:"title"` -} - -func (response ActivateServiceForSubject400ApplicationProblemPlusJSONResponse) VisitActivateServiceForSubjectResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/problem+json") - w.WriteHeader(400) - - return json.NewEncoder(w).Encode(response) -} - -type ActivateServiceForSubject412ApplicationProblemPlusJSONResponse struct { - // Detail A human-readable explanation specific to this occurrence of the problem. - Detail string `json:"detail"` - - // Status HTTP statuscode - Status float32 `json:"status"` - - // Title A short, human-readable summary of the problem type. - Title string `json:"title"` -} - -func (response ActivateServiceForSubject412ApplicationProblemPlusJSONResponse) VisitActivateServiceForSubjectResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/problem+json") - w.WriteHeader(412) - - return json.NewEncoder(w).Encode(response) -} - type ActivateServiceForSubjectdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. @@ -499,13 +445,13 @@ type StrictServerInterface interface { // Searches for presentations registered on the Discovery Service. // (GET /internal/discovery/v1/{serviceID}) SearchPresentations(ctx context.Context, request SearchPresentationsRequestObject) (SearchPresentationsResponseObject, error) - // Client API to deactivate the given subject from the Discovery Service. + // Remove a subject from the Discovery Service. // (DELETE /internal/discovery/v1/{serviceID}/{subjectID}) DeactivateServiceForSubject(ctx context.Context, request DeactivateServiceForSubjectRequestObject) (DeactivateServiceForSubjectResponseObject, error) // Retrieves the activation status of a subject on a Discovery Service. // (GET /internal/discovery/v1/{serviceID}/{subjectID}) GetServiceActivation(ctx context.Context, request GetServiceActivationRequestObject) (GetServiceActivationResponseObject, error) - // Client API to activate a subject on the specified Discovery Service. + // Activate a Discovery Service for a subject. // (POST /internal/discovery/v1/{serviceID}/{subjectID}) ActivateServiceForSubject(ctx context.Context, request ActivateServiceForSubjectRequestObject) (ActivateServiceForSubjectResponseObject, error) } diff --git a/discovery/api/v1/types.go b/discovery/api/v1/types.go index e5db1857e..2c3c738ee 100644 --- a/discovery/api/v1/types.go +++ b/discovery/api/v1/types.go @@ -37,7 +37,7 @@ type GetServiceActivation200JSONResponseStatus string const ( // ServiceStatusActive is the status for an active service. - ServiceStatusActive = "active" + ServiceStatusActive GetServiceActivation200JSONResponseStatus = "active" // ServiceStatusError is the status for an inactive service. - ServiceStatusError = "error" + ServiceStatusError GetServiceActivation200JSONResponseStatus = "error" ) diff --git a/discovery/client.go b/discovery/client.go index 059181c44..84ec5c300 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -43,20 +43,7 @@ import ( // clientRegistrationManager is a client component, responsible for managing registrations on a Discovery Service. // It can refresh registered Verifiable Presentations when they are about to expire. -type clientRegistrationManager interface { - activate(ctx context.Context, serviceID, subjectID string, parameters map[string]interface{}) error - deactivate(ctx context.Context, serviceID, subjectID string) error - // refresh checks which Verifiable Presentations that are about to expire, and should be refreshed on the Discovery Service. - refresh(ctx context.Context, now time.Time) error - // validate validates all presentations that are not yet validated - validate() error - // removeRevoked removes all revoked presentations from the store - removeRevoked() error -} - -var _ clientRegistrationManager = &defaultClientRegistrationManager{} - -type defaultClientRegistrationManager struct { +type clientRegistrationManager struct { services map[string]ServiceDefinition store *sqlStore client client.HTTPClient @@ -66,8 +53,8 @@ type defaultClientRegistrationManager struct { verifier presentationVerifier } -func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver, verifier presentationVerifier) *defaultClientRegistrationManager { - return &defaultClientRegistrationManager{ +func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver, verifier presentationVerifier) *clientRegistrationManager { + return &clientRegistrationManager{ services: services, store: store, client: client, @@ -78,16 +65,12 @@ func newRegistrationManager(services map[string]ServiceDefinition, store *sqlSto } } -func (r *defaultClientRegistrationManager) activate(ctx context.Context, serviceID, subjectID string, parameters map[string]interface{}) error { - service, serviceExists := r.services[serviceID] - if !serviceExists { - return ErrServiceNotFound - } - subjectDIDs, err := r.subjectManager.ListDIDs(ctx, subjectID) +func (r *clientRegistrationManager) activate(ctx context.Context, serviceID, subjectID string, parameters map[string]interface{}) error { + service, subjectDIDs, err := r.getServiceAndSubject(ctx, serviceID, subjectID) if err != nil { return err } - // filter DIDs on DID methods supported by the service + // filter DIDs on DID methods supported by the service; len == 0 means all DID Methods are accepted if len(service.DIDMethods) > 0 { j := 0 for i, did := range subjectDIDs { @@ -97,10 +80,6 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service } } subjectDIDs = subjectDIDs[:j] - - if len(subjectDIDs) == 0 { - return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrDIDMethodsNotSupported, subjectID) - } } // and filter by deactivated status @@ -116,7 +95,7 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service subjectDIDs = subjectDIDs[:j] if len(subjectDIDs) == 0 { - return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, didsubject.ErrSubjectNotFound, subjectID) + return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrNoSupportedDIDMethods, subjectID) } log.Logger().Debugf("Registering Verifiable Presentation on Discovery Service (service=%s, subject=%s)", service.ID, subjectID) @@ -162,23 +141,18 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service return nil } -func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, serviceID, subjectID string) error { - service, serviceExists := r.services[serviceID] - if !serviceExists { - return ErrServiceNotFound - } - // delete DID/service combination from DB, so it won't be registered again - err := r.store.updatePresentationRefreshTime(serviceID, subjectID, nil, nil) +func (r *clientRegistrationManager) deactivate(ctx context.Context, serviceID, subjectID string) error { + service, subjectDIDs, err := r.getServiceAndSubject(ctx, serviceID, subjectID) if err != nil { return err } - // subject is now successfully deactivated for the service, anything after this point is best effort - subjectDIDs, err := r.subjectManager.ListDIDs(ctx, subjectID) + // delete DID/service combination from DB, so it won't be registered again + err = r.store.updatePresentationRefreshTime(serviceID, subjectID, nil, nil) if err != nil { - // this could be a didsubject.ErrSubjectNotFound after the subject has been deactivated - // still fail in this case since we no longer have the keys to sign a retraction return err } + // subject is now successfully deactivated for the service, + // anything after this point is best-effort and should include ErrPresentationRegistrationFailed to trigger a 202 status code // filter DIDs on DID methods supported by the service if len(service.DIDMethods) > 0 { @@ -193,7 +167,7 @@ func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, servi } if len(subjectDIDs) == 0 { // if this means we can't deactivate a previously registered subject because the DID methods have changed, then we rely on the refresh interval to clean up. - return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrDIDMethodsNotSupported, subjectID) + return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrNoSupportedDIDMethods, subjectID) } // find all active presentations @@ -224,7 +198,20 @@ func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, servi return nil } -func (r *defaultClientRegistrationManager) deregisterPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, vp vc.VerifiablePresentation) error { +// getServiceAndSubject returns the service and subject, or ErrServiceNotFound / didsubject.ErrSubjectNotFound if either does not exist +func (r *clientRegistrationManager) getServiceAndSubject(ctx context.Context, serviceID, subjectID string) (ServiceDefinition, []did.DID, error) { + service, serviceExists := r.services[serviceID] + if !serviceExists { + return ServiceDefinition{}, nil, ErrServiceNotFound + } + subjectDIDs, err := r.subjectManager.ListDIDs(ctx, subjectID) + if err != nil { + return ServiceDefinition{}, nil, err + } + return service, subjectDIDs, nil +} + +func (r *clientRegistrationManager) deregisterPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, vp vc.VerifiablePresentation) error { presentation, err := r.buildPresentation(ctx, subjectDID, service, nil, map[string]interface{}{ "retract_jti": vp.ID.String(), }, &retractionPresentationType) @@ -234,7 +221,7 @@ func (r *defaultClientRegistrationManager) deregisterPresentation(ctx context.Co return r.client.Register(ctx, service.Endpoint, *presentation) } -func (r *defaultClientRegistrationManager) registerPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, parameters map[string]interface{}) error { +func (r *clientRegistrationManager) registerPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, parameters map[string]interface{}) error { presentation, err := r.findCredentialsAndBuildPresentation(ctx, subjectDID, service, parameters) if err != nil { return err @@ -242,7 +229,7 @@ func (r *defaultClientRegistrationManager) registerPresentation(ctx context.Cont return r.client.Register(ctx, service.Endpoint, *presentation) } -func (r *defaultClientRegistrationManager) findCredentialsAndBuildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, parameters map[string]interface{}) (*vc.VerifiablePresentation, error) { +func (r *clientRegistrationManager) findCredentialsAndBuildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, parameters map[string]interface{}) (*vc.VerifiablePresentation, error) { credentials, err := r.vcr.Wallet().List(ctx, subjectDID) if err != nil { return nil, err @@ -266,7 +253,7 @@ func (r *defaultClientRegistrationManager) findCredentialsAndBuildPresentation(c return r.buildPresentation(ctx, subjectDID, service, matchingCredentials, nil, nil) } -func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, credentials []vc.VerifiableCredential, additionalProperties map[string]interface{}, additionalVPType *ssi.URI) (*vc.VerifiablePresentation, error) { +func (r *clientRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, credentials []vc.VerifiableCredential, additionalProperties map[string]interface{}, additionalVPType *ssi.URI) (*vc.VerifiablePresentation, error) { nonce := nutsCrypto.GenerateNonce() // Make sure the presentation is not valid for longer than the max validity as defined by the Service Definitio. expires := time.Now().Add(time.Duration(service.PresentationMaxValidity-1) * time.Second).Truncate(time.Second) @@ -289,7 +276,8 @@ func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context }, &subjectDID, false) } -func (r *defaultClientRegistrationManager) refresh(ctx context.Context, now time.Time) error { +// refresh checks which Verifiable Presentations that are about to expire, and should be refreshed on the Discovery Service. +func (r *clientRegistrationManager) refresh(ctx context.Context, now time.Time) error { log.Logger().Debug("Refreshing own registered Verifiable Presentations on Discovery Services") refreshCandidates, err := r.store.getSubjectsToBeRefreshed(now) if err != nil { @@ -299,11 +287,13 @@ func (r *defaultClientRegistrationManager) refresh(ctx context.Context, now time for _, candidate := range refreshCandidates { var loopErr error if err = r.activate(ctx, candidate.ServiceID, candidate.SubjectID, candidate.Parameters); err != nil { - if errors.Is(err, ErrDIDMethodsNotSupported) { + if errors.Is(err, ErrNoSupportedDIDMethods) { // DID method no longer supported, remove err = r.store.updatePresentationRefreshTime(candidate.ServiceID, candidate.SubjectID, nil, nil) if err != nil { loopErr = fmt.Errorf("failed to remove subject with unsupported DID method (service=%s, subject=%s): %w", candidate.ServiceID, candidate.SubjectID, err) + } else { + loopErr = fmt.Errorf("removed subject that has no supported DID method (service=%s, subject=%s)", candidate.ServiceID, candidate.SubjectID) } } else if errors.Is(err, didsubject.ErrSubjectNotFound) { // Subject has probably been deactivated. Remove from service or registration will be retried every refresh interval. @@ -329,7 +319,8 @@ func (r *defaultClientRegistrationManager) refresh(ctx context.Context, now time return nil } -func (r *defaultClientRegistrationManager) validate() error { +// validate validates all presentations that are not yet validated +func (r *clientRegistrationManager) validate() error { errMsg := "background verification of presentation failed (service: %s, id: %s)" // find all unvalidated entries in store presentations, err := r.store.allPresentations(false) @@ -362,7 +353,8 @@ func (r *defaultClientRegistrationManager) validate() error { return nil } -func (r *defaultClientRegistrationManager) removeRevoked() error { +// removeRevoked removes all revoked presentations from the store +func (r *clientRegistrationManager) removeRevoked() error { errMsg := "background revocation check of presentation failed (id: %s)" // find all validated entries in store presentations, err := r.store.allPresentations(true) diff --git a/discovery/client_test.go b/discovery/client_test.go index 928722e41..cbebab6ad 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -52,7 +52,7 @@ type testContext struct { wallet *holder.MockWallet subjectManager *didsubject.MockManager store *sqlStore - manager *defaultClientRegistrationManager + manager *clientRegistrationManager } func newTestContext(t *testing.T) testContext { @@ -131,7 +131,7 @@ func Test_defaultClientRegistrationManager_activate(t *testing.T) { err := ctx.manager.activate(audit.TestContext(), unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) - assert.ErrorIs(t, err, ErrDIDMethodsNotSupported) + assert.ErrorIs(t, err, ErrNoSupportedDIDMethods) }) t.Run("no matching credentials", func(t *testing.T) { ctx := newTestContext(t) @@ -255,7 +255,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { err := ctx.manager.deactivate(audit.TestContext(), unsupportedServiceID, aliceSubject) - assert.ErrorIs(t, err, ErrDIDMethodsNotSupported) + assert.ErrorIs(t, err, ErrNoSupportedDIDMethods) }) t.Run("deregistering from Discovery Service fails", func(t *testing.T) { ctx := newTestContext(t) @@ -289,6 +289,13 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) }) + t.Run("unknown service", func(t *testing.T) { + ctx := newTestContext(t) + + err := ctx.manager.deactivate(audit.TestContext(), "unknown", aliceSubject) + + assert.ErrorIs(t, err, ErrServiceNotFound) + }) } func Test_defaultClientRegistrationManager_refresh(t *testing.T) { @@ -342,7 +349,7 @@ func Test_defaultClientRegistrationManager_refresh(t *testing.T) { assert.EqualError(t, err, "removed unknown subject (service=usecase_v1, subject=alice)") }) - t.Run("deactivate deactivated DID", func(t *testing.T) { + t.Run("deactivate unsupported DID method", func(t *testing.T) { ctx := newTestContext(t) ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, resolver.ErrDeactivated) @@ -350,18 +357,9 @@ func Test_defaultClientRegistrationManager_refresh(t *testing.T) { err := ctx.manager.refresh(audit.TestContext(), time.Now()) - assert.EqualError(t, err, "removed unknown subject (service=usecase_v1, subject=alice)") - }) - t.Run("deactivate unsupported DID method", func(t *testing.T) { - ctx := newTestContext(t) - ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - _ = ctx.store.updatePresentationRefreshTime(unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) - - err := ctx.manager.refresh(audit.TestContext(), time.Now()) - // refresh clears the registration - require.NoError(t, err) - record, err := ctx.store.getPresentationRefreshRecord(unsupportedServiceID, aliceSubject) + assert.EqualError(t, err, "removed subject that has no supported DID method (service=usecase_v1, subject=alice)") + record, err := ctx.store.getPresentationRefreshRecord(testServiceID, aliceSubject) assert.NoError(t, err) assert.Nil(t, record) }) @@ -395,19 +393,19 @@ func Test_defaultClientRegistrationManager_validate(t *testing.T) { tests := []struct { name string - setupManager func(ctx testContext) *defaultClientRegistrationManager + setupManager func(ctx testContext) *clientRegistrationManager expectedLen int }{ { name: "ok", - setupManager: func(ctx testContext) *defaultClientRegistrationManager { + setupManager: func(ctx testContext) *clientRegistrationManager { return ctx.manager }, expectedLen: 1, }, { name: "verification failed", - setupManager: func(ctx testContext) *defaultClientRegistrationManager { + setupManager: func(ctx testContext) *clientRegistrationManager { return newRegistrationManager(testDefinitions(), ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver, func(service ServiceDefinition, vp vc.VerifiablePresentation) error { return errors.New("verification failed") }) @@ -416,7 +414,7 @@ func Test_defaultClientRegistrationManager_validate(t *testing.T) { }, { name: "registration for unknown service", - setupManager: func(ctx testContext) *defaultClientRegistrationManager { + setupManager: func(ctx testContext) *clientRegistrationManager { return newRegistrationManager(map[string]ServiceDefinition{}, ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver, alwaysOkVerifier) }, expectedLen: 0, diff --git a/discovery/definition.go b/discovery/definition.go index fe84531ba..e032ae696 100644 --- a/discovery/definition.go +++ b/discovery/definition.go @@ -50,7 +50,7 @@ type ServiceDefinition struct { ID string `json:"id"` // DIDMethods is a list of DID methods that are supported by the use case. // If empty, all methods are supported. - DIDMethods []string `json:"did_methods"` + DIDMethods []string `json:"did_methods,omitempty"` // Endpoint is the endpoint where the use case list is served. Endpoint string `json:"endpoint"` // PresentationDefinition specifies the Presentation ServiceDefinition submissions to the list must conform to, diff --git a/discovery/interface.go b/discovery/interface.go index 7a9cf5e92..7e1d00e7b 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -34,8 +34,12 @@ var ErrPresentationAlreadyExists = errors.New("presentation already exists") // ErrPresentationRegistrationFailed indicates registration of a presentation on a remote Discovery Service failed. var ErrPresentationRegistrationFailed = errors.New("registration of Verifiable Presentation on remote Discovery Service failed") +// ErrDIDMethodsNotSupported indicates that a received VP does not match the supported DID Methods of the service. var ErrDIDMethodsNotSupported = errors.New("DID methods not supported") +// ErrNoSupportedDIDMethods indicates that the client cannot create a VP for a subject because it has no (active) DID matching the supported DID Methods of the service. +var ErrNoSupportedDIDMethods = errors.New("subject has no (active) DIDs matching the service") + // authServerURLField is the field name for the authServerURL in the DiscoveryRegistrationCredential. // it is used to resolve authorization server metadata and thus the endpoints for a service entry. const authServerURLField = "authServerURL" @@ -55,19 +59,20 @@ type Server interface { type Client interface { // Search searches for presentations which credential(s) match the given query. // Query parameters are formatted as simple JSON paths, e.g. "issuer" or "credentialSubject.name". + // It returns an ErrServiceNotFound if the service invalid/unknown. Search(serviceID string, query map[string]string) ([]SearchResult, error) // ActivateServiceForSubject causes a subject to be registered for a Discovery Service. // Registration of all DIDs of the subject will be attempted immediately, and automatically refreshed. // If the function is called again for the same service/DID combination, it will try to refresh the registration. // parameters are added as credentialSubject to a DiscoveryRegistrationCredential holder credential. - // It returns an error if the service or subject is invalid/unknown. + // It returns an ErrServiceNotFound or didsubject.ErrSubjectNotFound if the service or subject is invalid/unknown. ActivateServiceForSubject(ctx context.Context, serviceID, subjectID string, parameters map[string]interface{}) error // DeactivateServiceForSubject stops registration of a subject on a Discovery Service. // It also tries to remove all active registrations of the subject from the Discovery Service. // If removal of one or more active registration fails a ErrPresentationRegistrationFailed may be returned. The failed registrations will be removed when they expire. - // It returns an error if the service or subject is invalid/unknown. + // It returns an ErrServiceNotFound or didsubject.ErrSubjectNotFound if the service or subject is invalid/unknown. DeactivateServiceForSubject(ctx context.Context, serviceID, subjectID string) error // Services returns the list of services that are registered on this client. @@ -76,7 +81,8 @@ type Client interface { // GetServiceActivation returns the activation status of a subject on a Discovery Service. // The boolean indicates whether the subject is activated on the Discovery Service (ActivateServiceForSubject() has been called). // It also returns the Verifiable Presentations for all DIDs of the subject that are registered on the Discovery Service, if any. - // It returns a refreshRecordError if the last refresh of the service failed (activation status and VPs are still returned). + // It returns an ErrServiceNotFound or didsubject.ErrSubjectNotFound if the service or subject is invalid/unknown. + // It returns a RegistrationRefreshError with additional information if the last refresh of the service failed (activation status and VPs are still returned). // The time of the last error is added in the error message. GetServiceActivation(ctx context.Context, serviceID, subjectID string) (bool, []vc.VerifiablePresentation, error) } diff --git a/discovery/module.go b/discovery/module.go index 86d5ef678..c979fb9e7 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -86,7 +86,7 @@ type Module struct { httpClient client.HTTPClient storageInstance storage.Engine store *sqlStore - registrationManager clientRegistrationManager + registrationManager *clientRegistrationManager serverDefinitions map[string]ServiceDefinition allDefinitions map[string]ServiceDefinition vcrInstance vcr.VCR @@ -106,7 +106,7 @@ func (m *Module) Configure(serverConfig core.ServerConfig) error { return err } - m.httpClient = client.New(serverConfig.Strictmode, serverConfig.HTTPClient.Timeout, nil) + m.httpClient = client.New(serverConfig.HTTPClient.Timeout) return m.loadDefinitions() @@ -390,7 +390,10 @@ func (m *Module) ActivateServiceForSubject(ctx context.Context, serviceID, subje } log.Logger().Infof("Successfully activated service for subject (subject=%s,service=%s)", subjectID, serviceID) - _ = m.clientUpdater.updateService(ctx, m.allDefinitions[serviceID]) + err = m.clientUpdater.updateService(ctx, m.allDefinitions[serviceID]) + if err != nil { + log.Logger().Infof("Failed to update local copy of Discovery Service (service=%s): %s", serviceID, err) + } return nil } @@ -412,6 +415,11 @@ func (m *Module) Services() []ServiceDefinition { // GetServiceActivation is a Discovery Client function that retrieves the activation status of a service for a subject. // See interface.go for more information. func (m *Module) GetServiceActivation(ctx context.Context, serviceID, subjectID string) (bool, []vc.VerifiablePresentation, error) { + // first check if the combination getServiceAndSubject to generate correct api returns + _, subjectDIDs, err := m.registrationManager.getServiceAndSubject(ctx, serviceID, subjectID) + if err != nil { + return false, nil, err + } refreshRecord, err := m.store.getPresentationRefreshRecord(serviceID, subjectID) if err != nil { return false, nil, err @@ -421,12 +429,6 @@ func (m *Module) GetServiceActivation(ctx context.Context, serviceID, subjectID } // subject is activated for service - subjectDIDs, err := m.subjectManager.ListDIDs(ctx, subjectID) - if err != nil { - // can only happen if DB is offline/corrupt, or between deactivating a subject and its next refresh on the service (didsubject.ErrSubjectNotFound) - return true, nil, err - } - vps2D, err := m.store.getSubjectVPsOnService(serviceID, subjectDIDs) if err != nil { return true, nil, err // DB err diff --git a/discovery/module_test.go b/discovery/module_test.go index e04ce0ff5..52dc2c2ba 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -40,7 +40,6 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "gorm.io/gorm" - "os" "sync" "sync/atomic" "testing" @@ -464,8 +463,8 @@ func TestModule_Search(t *testing.T) { { Presentation: vpAlice, Fields: map[string]interface{}{ - "auth_server_url":"https://example.com/oauth2/alice", - "issuer_field": authorityDID, + "auth_server_url": "https://example.com/oauth2/alice", + "issuer_field": authorityDID, }, Parameters: defaultRegistrationParams(aliceSubject), }, @@ -625,7 +624,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { require.EqualError(t, err, "subject not found") }) - t.Run("deactivated", func(t *testing.T) { + t.Run("deactivated DID", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) m, testContext := setupModule(t, storageEngine) @@ -634,7 +633,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, nil) - assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) + assert.ErrorIs(t, err, ErrNoSupportedDIDMethods) }) t.Run("ok, but couldn't register presentation -> maps to ErrRegistrationFailed", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) @@ -667,7 +666,8 @@ func TestModule_GetServiceActivation(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) t.Run("not activated", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, ctx := setupModule(t, storageEngine) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) activated, presentation, err := m.GetServiceActivation(context.Background(), testServiceID, aliceSubject) @@ -713,14 +713,23 @@ func TestModule_GetServiceActivation(t *testing.T) { assert.ErrorAs(t, err, &RegistrationRefreshError{}) }) }) -} + t.Run("service does not exist - 404", func(t *testing.T) { + m, _ := setupModule(t, storageEngine) -func checkWriteAccess(dir string) bool { - info, err := os.Stat(dir) - if err != nil { - return false - } + activated, presentation, err := m.GetServiceActivation(context.Background(), "unknown", "unknown") + + assert.ErrorIs(t, err, ErrServiceNotFound) + assert.False(t, activated) + assert.Nil(t, presentation) + }) + t.Run("subject does not exist - 404", func(t *testing.T) { + m, ctx := setupModule(t, storageEngine) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), "unknown").Return(nil, didsubject.ErrSubjectNotFound) + + activated, presentation, err := m.GetServiceActivation(context.Background(), testServiceID, "unknown") - // Check if the directory is writable by the current user - return info.Mode().Perm()&(1<<(uint(7))) != 0 + assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) + assert.False(t, activated) + assert.Nil(t, presentation) + }) } diff --git a/docs/_static/discovery/v1.yaml b/docs/_static/discovery/v1.yaml index d56076d6f..e8b597d4c 100644 --- a/docs/_static/discovery/v1.yaml +++ b/docs/_static/discovery/v1.yaml @@ -70,6 +70,9 @@ paths: - `credentialSubject.organization.name=Hospital*` - `credentialSubject.organization.name=*clinic` - `issuer=did:web:example.com` + + error returns: + * 404 - unknown service. operationId: searchPresentations tags: - discovery @@ -111,6 +114,9 @@ paths: and the status of the activation. A refresh could have failed. It will return true after successfully calling the activateServiceForSubject API, and false after calling the deactivateServiceForSubject API. It also returns the active Verifiable Presentations, if any. + + error returns: + * 404 - unknown service or subject. operationId: getServiceActivation tags: - discovery @@ -123,7 +129,6 @@ paths: type: object required: - activated - - status properties: activated: type: boolean @@ -140,31 +145,30 @@ paths: vp: description: | List of VPs on the Discovery Service for the subject. One per DID method registered on the Service. - The list can be empty even if activated==true if none of the DIDs of a subject is actually registered on the Discovery Service. + The list is empty when status is "error". type: array items: $ref: "#/components/schemas/VerifiablePresentation" default: $ref: "../common/error_response.yaml" post: - summary: Client API to activate a subject on the specified Discovery Service. + summary: Activate a Discovery Service for a subject. description: | An API provided by the discovery client that will cause all qualifying DIDs of a subject to be registered on the specified Discovery Service. A DID qualifies for registration if it meets the requirements defined the Presentation Definition of the Discovery Service. - Registration of all DIDs of a subject will be attempted immediately, and they will be automatically refreshed. + Registration of all DIDs of a subject will be attempted immediately. + If at least one DID is registered on the Discovery Server, the operation is considered a success and will be periodically refreshed for the entire subject. Applications only need to call this API once for every service/subject combination, until the registration is explicitly deleted through this API. - If initial registration fails, this API returns the error indicating what failed and periodically retry registration. Applications can force a retry by calling this API again. error returns: - * 400 - incorrect input: invalid/unknown service or subject. - * 412 - precondition failed: subject doesn't have the required credentials. + * 404 - unknown service or subject + * 412 - precondition failed: subject doesn't have the required credentials operationId: activateServiceForSubject tags: - discovery requestBody: - required: true content: application/json: schema: @@ -172,21 +176,16 @@ paths: responses: "200": description: Activation was successful. - - "400": - $ref: "../common/error_response.yaml" - "412": - $ref: "../common/error_response.yaml" default: $ref: "../common/error_response.yaml" delete: - summary: Client API to deactivate the given subject from the Discovery Service. + summary: Remove a subject from the Discovery Service. description: | An API provided by the discovery client that will cancel the periodic registration of a subject on the specified Discovery Service. It will also try to delete all the existing registrations on the Discovery Service, if any. error returns: - * 400 - incorrect input: invalid/unknown service or subject. + * 404 - unknown service or subject operationId: deactivateServiceForSubject tags: - discovery @@ -209,8 +208,6 @@ paths: reason: type: string description: Description of why removal of the registration failed. - "400": - $ref: "../common/error_response.yaml" default: $ref: "../common/error_response.yaml" components: @@ -269,6 +266,11 @@ components: id: type: string description: The ID of the Discovery Service. + did_methods: + type: array + items: + type: string + description: List of DID Methods supported by the Discovery Service. Empty/missing means no restrictions. endpoint: type: string description: The endpoint of the Discovery Service. diff --git a/e2e-tests/browser/client/iam/generated.go b/e2e-tests/browser/client/iam/generated.go index a4fffed9f..d264b86e7 100644 --- a/e2e-tests/browser/client/iam/generated.go +++ b/e2e-tests/browser/client/iam/generated.go @@ -85,7 +85,7 @@ type ExtendedTokenIntrospectionResponse struct { // Aud RFC7662 - Service-specific string identifier or list of string identifiers representing the intended audience for this token, as defined in JWT [RFC7519]. Aud *string `json:"aud,omitempty"` - // ClientId The client (DID) the access token was issued to + // ClientId The client identity the access token was issued to. Since the Verifiable Presentation is used to grant access, the client_id reflects the client_id in the access token request. ClientId *string `json:"client_id,omitempty"` // Cnf The 'confirmation' claim is used in JWTs to proof the possession of a key. @@ -97,7 +97,7 @@ type ExtendedTokenIntrospectionResponse struct { // Iat Issuance time in seconds since UNIX epoch Iat *int `json:"iat,omitempty"` - // Iss Contains the DID of the authorizer. Should be equal to 'sub' + // Iss Issuer URL of the authorizer. Iss *string `json:"iss,omitempty"` // PresentationDefinitions Presentation Definitions, as described in Presentation Exchange specification, fulfilled to obtain the access token diff --git a/vdr/api/v2/generated.go b/vdr/api/v2/generated.go index 8e05597ab..11a3fd99f 100644 --- a/vdr/api/v2/generated.go +++ b/vdr/api/v2/generated.go @@ -35,6 +35,7 @@ type CreateSubjectOptions struct { Keys *KeyCreationOptions `json:"keys,omitempty"` // Subject controls the DID subject to which all created DIDs are bound. If not given, a uuid is generated and returned. + // The subject must follow the pattern [a-zA-Z0-9._-]+ Subject *string `json:"subject,omitempty"` } From 0b61a032424588156ae699c13cf94d2e879132d2 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 25 Oct 2024 10:36:42 +0200 Subject: [PATCH 66/72] docs: v6 release date (#3519) * v6 release date * Update docs/pages/release_notes.rst Co-authored-by: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> --------- Co-authored-by: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> --- docs/index.rst | 6 ++++++ docs/pages/release_notes.rst | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 041c5ca17..7e8f86468 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,6 +4,12 @@ Nuts documentation ################## +.. note:: + + This version of the documentation is for Nuts v6.0.0. + It no longer describes the setup of the Nuts network required for did:nuts. + If you're looking for information on setting up a Nuts network, please refer to the `previous version `_. + .. toctree:: :maxdepth: 1 :caption: Integrating: diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index 3d6eb939e..92360079f 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -3,11 +3,12 @@ Release notes ############# ******************* -Peanut (6.0.0) +Peanut (v6.0.0) ******************* -**Release date:** TBD -**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v5.0.0...v6.0.0 +Release date: 2024-10-25 + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v5.4.0...v6.0.0 ================ Breaking changes From bd23e97883edcb2578b55b40e66fc9c54dc7176b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:58:25 +0200 Subject: [PATCH 67/72] Bump github.com/lestrrat-go/jwx/v2 from 2.1.1 to 2.1.2 (#3520) Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/lestrrat-go/jwx/releases) - [Changelog](https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes) - [Commits](https://github.com/lestrrat-go/jwx/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: github.com/lestrrat-go/jwx/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d272b30c5..0770e663b 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/knadh/koanf/providers/structs v0.1.0 github.com/knadh/koanf/v2 v2.1.1 github.com/labstack/echo/v4 v4.12.0 - github.com/lestrrat-go/jwx/v2 v2.1.1 + github.com/lestrrat-go/jwx/v2 v2.1.2 github.com/magiconair/properties v1.8.7 github.com/mdp/qrterminal/v3 v3.2.0 github.com/mr-tron/base58 v1.2.0 diff --git a/go.sum b/go.sum index 238882d53..ce11ff6b8 100644 --- a/go.sum +++ b/go.sum @@ -275,8 +275,8 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.1 h1:Y2ltVl8J6izLYFs54BVcpXLv5msSW4o8eXwnzZLI32E= -github.com/lestrrat-go/jwx/v2 v2.1.1/go.mod h1:4LvZg7oxu6Q5VJwn7Mk/UwooNRnTHUpXBj2C4j3HNx0= +github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc= +github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= From 71c2beb56ee1eb919cff2efb2dd48ffb1681aa65 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:58:58 +0100 Subject: [PATCH 68/72] Add gh action for CodeQL schedule (#3523) * Add gh action for CodeQL schedule * change branch matrix * pr feedback * revert matrix change --- .../codeql-analysis-cron-schedule.yml | 65 +++++++++++++++++++ .github/workflows/codeql-analysis.yml | 2 - 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/codeql-analysis-cron-schedule.yml diff --git a/.github/workflows/codeql-analysis-cron-schedule.yml b/.github/workflows/codeql-analysis-cron-schedule.yml new file mode 100644 index 000000000..8bd568845 --- /dev/null +++ b/.github/workflows/codeql-analysis-cron-schedule.yml @@ -0,0 +1,65 @@ +# This is an alternative to the codeql-analysis.yml that only contains a scheduled evaluation of CodeQL + +name: "Scheduled CodeQL" + +on: + schedule: + - cron: '21 10 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + branches: + - 'master' + - 'V5.4' + - 'V6.0' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ matrix.branches }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + # use go version from go.mod. + go-version-file: 'go.mod' + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: 'go' + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4cf60cc8f..51e6a54af 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,8 +21,6 @@ on: branches: - 'master' - 'V*' - schedule: - - cron: '21 10 * * 2' jobs: analyze: From 390db78483566251421e53f1c0189e64a29f826b Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:33:23 +0100 Subject: [PATCH 69/72] change cron schedule (#3524) * change cron schedule * / is not documented in POSIX cron sytax supported by gh actions --- .github/workflows/codeql-analysis-cron-schedule.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis-cron-schedule.yml b/.github/workflows/codeql-analysis-cron-schedule.yml index 8bd568845..017093a09 100644 --- a/.github/workflows/codeql-analysis-cron-schedule.yml +++ b/.github/workflows/codeql-analysis-cron-schedule.yml @@ -4,7 +4,7 @@ name: "Scheduled CodeQL" on: schedule: - - cron: '21 10 * * 1' + - cron: '0,15,30,45 * * * *' jobs: analyze: From 12e4cb489f3f38c4b0ac8a6753640ca8336dffb7 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:51:28 +0100 Subject: [PATCH 70/72] Schedule CodeQL twice a week (#3525) * schedule CodeQL twice a week * add comment on schedule * add more comments --- .github/workflows/codeql-analysis-cron-schedule.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis-cron-schedule.yml b/.github/workflows/codeql-analysis-cron-schedule.yml index 017093a09..c3fe7e0ed 100644 --- a/.github/workflows/codeql-analysis-cron-schedule.yml +++ b/.github/workflows/codeql-analysis-cron-schedule.yml @@ -1,10 +1,13 @@ # This is an alternative to the codeql-analysis.yml that only contains a scheduled evaluation of CodeQL +# The action runs for all branches defined in jobs.analyze.strategy.matrix.branches. +# Every new production branch (minor release branches) should be added to this list. name: "Scheduled CodeQL" +# run twice a week at a random time on Sunday and Wednesday evening so its available the next morning on: schedule: - - cron: '0,15,30,45 * * * *' + - cron: '42 21 * * 0,3' jobs: analyze: @@ -18,6 +21,7 @@ jobs: strategy: fail-fast: false matrix: + # CodeQL runs on these branches branches: - 'master' - 'V5.4' From 8d74a2aa8d4870653f8d7af196cee2b508be7bd4 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Thu, 31 Oct 2024 11:09:00 +0100 Subject: [PATCH 71/72] PEX: Return capture group for matched patterns (#3526) --- docs/pages/deployment/pex.rst | 29 +++++++++++++ vcr/pe/presentation_definition.go | 60 +++++++++++++++++--------- vcr/pe/presentation_definition_test.go | 51 +++++++++++++++++++--- 3 files changed, 113 insertions(+), 27 deletions(-) diff --git a/docs/pages/deployment/pex.rst b/docs/pages/deployment/pex.rst index ba2b2314a..4efa271d5 100644 --- a/docs/pages/deployment/pex.rst +++ b/docs/pages/deployment/pex.rst @@ -95,3 +95,32 @@ Writer of policies should take into consideration: - fields that are intended to be used for logging or authorization decisions should have a distinct identifier. - claims ideally map a registered claim name (e.g. `IANA JWT claims `_) - overwriting properties already defined in the token introspection endpoint response is forbidden. These are: ``iss``, ``sub``, ``exp``, ``iat``, ``active``, ``client_id``, ``scope``. + +Extracting substrings with regular expressions +============================================== +If you want introspection to return part of a string, you can use the ``pattern`` regular expression filter in the field definition with a capture group. +Token introspection will return the value of the capture group in the regular expression, instead of the whole field value. +For instance, if you want to extract the level from the string ``"Admin level 4"`` from the following credential: + +.. code-block:: json + + { + "credentialSubject": { + "role": "Admin level 4" + } + } + +You can define the following field in the input descriptor constraint, to have the level returned in the introspection response as ``admin_level``: + +.. code-block:: json + + { + "id": "admin_level", + "path": ["$.credentialSubject.role"], + "filter": { + "type": "string" + "pattern": "Admin level ([0-9])" + } + } + +Only 1 capture group is supported in regular expressions. If multiple capture groups are defined, an error will be returned. \ No newline at end of file diff --git a/vcr/pe/presentation_definition.go b/vcr/pe/presentation_definition.go index b3b27bfe3..df41b6907 100644 --- a/vcr/pe/presentation_definition.go +++ b/vcr/pe/presentation_definition.go @@ -435,12 +435,12 @@ func matchField(field Field, credential map[string]interface{}) (bool, interface } // if filter at path matches return true - match, err := matchFilter(*field.Filter, value) + match, matchedValue, err := matchFilter(*field.Filter, value) if err != nil { return false, nil, err } if match { - return true, value, nil + return true, matchedValue, nil } // if filter at path does not match continue and set optionalInvalid optionalInvalid++ @@ -466,14 +466,17 @@ func getValueAtPath(path string, vcAsInterface interface{}) (interface{}, error) return value, err } -// matchFilter matches the value against the filter. +// matchFilter matches the value against the filter. It returns true if the value matches the filter, along with the matched value. // A filter is a JSON Schema descriptor (https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural) // Supported schema types: string, number, boolean, array, enum. // Supported schema properties: const, enum, pattern. These only work for strings. // Supported go value types: string, float64, int, bool and array. // 'null' values are not supported. -// It returns an error on unsupported features or when the regex pattern fails. -func matchFilter(filter Filter, value interface{}) (bool, error) { +// It returns an error when; +// - an unsupported feature is used +// - the regex pattern fails +// - the regex pattern contains more than 1 capture group +func matchFilter(filter Filter, value interface{}) (bool, interface{}, error) { // first we check if it's an enum, so we can recursively call matchFilter for each value if filter.Enum != nil { for _, enum := range filter.Enum { @@ -481,62 +484,79 @@ func matchFilter(filter Filter, value interface{}) (bool, error) { Type: "string", Const: &enum, } - match, _ := matchFilter(f, value) + match, result, _ := matchFilter(f, value) if match { - return true, nil + return true, result, nil } } - return false, nil + return false, nil, nil } switch typedValue := value.(type) { case string: if filter.Type != "string" { - return false, nil + return false, nil, nil } case float64: if filter.Type != "number" { - return false, nil + return false, nil, nil } case int: if filter.Type != "number" { - return false, nil + return false, nil, nil } case bool: if filter.Type != "boolean" { - return false, nil + return false, nil, nil } case []interface{}: for _, v := range typedValue { - match, err := matchFilter(filter, v) + match, _, err := matchFilter(filter, v) if err != nil { - return false, err + return false, nil, err } if match { - return true, nil + return true, value, nil } } default: // object not supported for now - return false, ErrUnsupportedFilter + return false, nil, ErrUnsupportedFilter } if filter.Const != nil { if value != *filter.Const { - return false, nil + return false, nil, nil } } if filter.Pattern != nil && filter.Type == "string" { re, err := regexp2.Compile(*filter.Pattern, regexp2.ECMAScript) if err != nil { - return false, err + return false, nil, err + } + match, err := re.FindStringMatch(value.(string)) + if err != nil { + return false, nil, err + } + if match == nil { + return false, nil, nil + } + // We support returning a single capture group; + // - If there's a capture group, return it + // - If there's no capture group, return the whole match + // - If there's multiple capture groups, return an error + if len(match.Groups()) == 1 { + return true, string(match.Capture.Runes()), nil + } else if len(match.Groups()) == 2 { + return true, string(match.Groups()[1].Runes()), nil + } else { + return false, nil, errors.New("can't return results from multiple regex capture groups") } - return re.MatchString(value.(string)) } // if we get here, no pattern, enum or const is requested just the type. - return true, nil + return true, value, nil } // deduplicate removes duplicate VCs from the slice. diff --git a/vcr/pe/presentation_definition_test.go b/vcr/pe/presentation_definition_test.go index b1570239e..902ad2dad 100644 --- a/vcr/pe/presentation_definition_test.go +++ b/vcr/pe/presentation_definition_test.go @@ -25,6 +25,7 @@ import ( "embed" "encoding/json" "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/nuts-node/core/to" vcrTest "github.com/nuts-foundation/nuts-node/vcr/test" "strings" "testing" @@ -761,9 +762,12 @@ func Test_matchFilter(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - got, err := matchFilter(testCase.filter, testCase.value) + matches, matchedValue, err := matchFilter(testCase.filter, testCase.value) require.NoError(t, err) - assert.Equal(t, testCase.want, got) + assert.Equal(t, testCase.want, matches) + if testCase.want && matches { + assert.Equal(t, testCase.value, matchedValue) + } }) } }) @@ -775,13 +779,46 @@ func Test_matchFilter(t *testing.T) { filters := []Filter{f1, f2, f3} t.Run("ok", func(t *testing.T) { for _, filter := range filters { - match, err := matchFilter(filter, stringValue) + match, matchedValue, err := matchFilter(filter, stringValue) require.NoError(t, err) assert.True(t, match) + assert.Equal(t, stringValue, matchedValue) } }) + t.Run("pattern", func(t *testing.T) { + t.Run("no match", func(t *testing.T) { + match, value, err := matchFilter(Filter{Type: "string", Pattern: to.Ptr("[0-9]+")}, "value") + require.NoError(t, err) + assert.Nil(t, value) + assert.False(t, match) + }) + t.Run("capture group", func(t *testing.T) { + match, value, err := matchFilter(Filter{Type: "string", Pattern: to.Ptr("v([a-z]+)e")}, "value") + require.NoError(t, err) + assert.Equal(t, "alu", value) + assert.True(t, match) + }) + t.Run("no capture group", func(t *testing.T) { + match, value, err := matchFilter(Filter{Type: "string", Pattern: to.Ptr("value")}, "value") + require.NoError(t, err) + assert.Equal(t, "value", value) + assert.True(t, match) + }) + t.Run("non-capturing group", func(t *testing.T) { + match, value, err := matchFilter(Filter{Type: "string", Pattern: to.Ptr("(?:val)ue")}, "value") + require.NoError(t, err) + assert.Equal(t, "value", value) + assert.True(t, match) + }) + t.Run("too many capture groups", func(t *testing.T) { + match, value, err := matchFilter(Filter{Type: "string", Pattern: to.Ptr("(v)(a)lue")}, "value") + require.EqualError(t, err, "can't return results from multiple regex capture groups") + assert.False(t, match) + assert.Nil(t, value) + }) + }) t.Run("enum value not found", func(t *testing.T) { - match, err := matchFilter(f2, "foo") + match, _, err := matchFilter(f2, "foo") require.NoError(t, err) assert.False(t, match) }) @@ -790,17 +827,17 @@ func Test_matchFilter(t *testing.T) { t.Run("error cases", func(t *testing.T) { t.Run("enum with wrong type", func(t *testing.T) { f := Filter{Type: "object"} - match, err := matchFilter(f, struct{}{}) + match, _, err := matchFilter(f, struct{}{}) assert.False(t, match) assert.Equal(t, err, ErrUnsupportedFilter) }) t.Run("incorrect regex", func(t *testing.T) { pattern := "[" f := Filter{Type: "string", Pattern: &pattern} - match, err := matchFilter(f, stringValue) + match, _, err := matchFilter(f, stringValue) assert.False(t, match) assert.Error(t, err, "error parsing regexp: missing closing ]: `[`") - match, err = matchFilter(f, []interface{}{stringValue}) + match, _, err = matchFilter(f, []interface{}{stringValue}) assert.False(t, match) assert.Error(t, err, "error parsing regexp: missing closing ]: `[`") }) From 3859302f7acedaba723af6eca77c9a7a54357004 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:07:11 +0100 Subject: [PATCH 72/72] PKI add ValidateStrict (#3531) * PKI add ValidateStrict * update test --- pki/interface.go | 4 ++++ pki/mock.go | 28 ++++++++++++++++++++++++++++ pki/validator.go | 10 +++++++++- pki/validator_test.go | 10 ++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/pki/interface.go b/pki/interface.go index 95fd2c026..8b3631309 100644 --- a/pki/interface.go +++ b/pki/interface.go @@ -63,9 +63,13 @@ type Validator interface { // ErrCRLMissing and ErrCRLExpired signal that at least one of the certificates cannot be validated reliably. // If the certificate was revoked on an expired CRL, it wil return ErrCertRevoked. // Ignoring all errors except ErrCertRevoked changes the behavior from hard-fail to soft-fail. Without a truststore, the Validator is a noop if set to soft-fail + // Validate uses the configured soft-/hard-fail strategy // The certificate chain is expected to be sorted leaf to root. Validate(chain []*x509.Certificate) error + // ValidateStrict does the same as Validate, except it always uses the hard-fail strategy. + ValidateStrict(chain []*x509.Certificate) error + // SetVerifyPeerCertificateFunc sets config.ValidatePeerCertificate to use Validate. SetVerifyPeerCertificateFunc(config *tls.Config) error diff --git a/pki/mock.go b/pki/mock.go index d8f2321aa..21d8c97a3 100644 --- a/pki/mock.go +++ b/pki/mock.go @@ -189,6 +189,20 @@ func (mr *MockValidatorMockRecorder) Validate(chain any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockValidator)(nil).Validate), chain) } +// ValidateStrict mocks base method. +func (m *MockValidator) ValidateStrict(chain []*x509.Certificate) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateStrict", chain) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateStrict indicates an expected call of ValidateStrict. +func (mr *MockValidatorMockRecorder) ValidateStrict(chain any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateStrict", reflect.TypeOf((*MockValidator)(nil).ValidateStrict), chain) +} + // MockProvider is a mock of Provider interface. type MockProvider struct { ctrl *gomock.Controller @@ -281,3 +295,17 @@ func (mr *MockProviderMockRecorder) Validate(chain any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockProvider)(nil).Validate), chain) } + +// ValidateStrict mocks base method. +func (m *MockProvider) ValidateStrict(chain []*x509.Certificate) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateStrict", chain) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateStrict indicates an expected call of ValidateStrict. +func (mr *MockProviderMockRecorder) ValidateStrict(chain any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateStrict", reflect.TypeOf((*MockProvider)(nil).ValidateStrict), chain) +} diff --git a/pki/validator.go b/pki/validator.go index e8e70dbd0..ad1a9da3f 100644 --- a/pki/validator.go +++ b/pki/validator.go @@ -128,6 +128,14 @@ func (v *validator) syncLoop(ctx context.Context) { } func (v *validator) Validate(chain []*x509.Certificate) error { + return v.validate(chain, v.softfail) +} + +func (v *validator) ValidateStrict(chain []*x509.Certificate) error { + return v.validate(chain, false) +} + +func (v *validator) validate(chain []*x509.Certificate, softfail bool) error { var cert *x509.Certificate var err error for i := range chain { @@ -135,7 +143,7 @@ func (v *validator) Validate(chain []*x509.Certificate) error { // check in reverse order to prevent CRL expiration errors due to revoked CAs no longer issuing CRLs if err = v.validateCert(cert); err != nil { errOut := fmt.Errorf("%w: subject=%s, S/N=%s, issuer=%s", err, cert.Subject.String(), cert.SerialNumber.String(), cert.Issuer.String()) - if v.softfail && !(errors.Is(err, ErrCertRevoked) || errors.Is(err, ErrCertBanned)) { + if softfail && !(errors.Is(err, ErrCertRevoked) || errors.Is(err, ErrCertBanned)) { // Accept the certificate even if it cannot be properly validated logger().WithError(errOut).Error("Certificate CRL check softfail bypass. Might be unsafe, find cause of failure!") continue diff --git a/pki/validator_test.go b/pki/validator_test.go index 081275afd..2e8e1f6bf 100644 --- a/pki/validator_test.go +++ b/pki/validator_test.go @@ -110,11 +110,21 @@ func TestValidator_Validate(t *testing.T) { assert.ErrorIs(t, err, expected) } } + fnStrict := func(expected error) { + val.softfail = true // make sure it ignores the configured value + err = val.ValidateStrict([]*x509.Certificate{cert}) + if expected == nil { + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, expected) + } + } t.Run("softfail", func(t *testing.T) { fn(true, softfailReturn) }) t.Run("hardfail", func(t *testing.T) { fn(false, hardfailReturn) + fnStrict(hardfailReturn) }) }