From 8b553fb06c31bb13a3f7c76faa94a505eb311365 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 3 Jan 2024 13:35:05 +0100 Subject: [PATCH 01/15] Discovery: DID registration by clients --- README.rst | 153 ++--- cmd/root.go | 2 +- discovery/api/v1/generated.go | 602 ++++++++++++++++++ discovery/api/v1/wrapper.go | 36 ++ discovery/api/v1/wrapper_test.go | 70 ++ discovery/client.go | 187 ++++++ discovery/client_test.go | 205 ++++++ discovery/cmd/cmd.go | 3 + discovery/config.go | 17 +- discovery/config_test.go | 10 + discovery/interface.go | 17 + discovery/mock.go | 30 + discovery/module.go | 59 +- discovery/module_test.go | 4 +- discovery/store.go | 50 ++ discovery/store_test.go | 52 ++ docs/_static/discovery/v1.yaml | 81 +++ docs/pages/deployment/cli-reference.rst | 145 ++--- docs/pages/deployment/server_options.rst | 177 ++--- .../discovery/definitions/definition.json | 52 ++ e2e-tests/discovery/docker-compose.yml | 40 ++ e2e-tests/discovery/node-A/nuts.yaml | 23 + e2e-tests/discovery/node-B/nuts.yaml | 25 + e2e-tests/discovery/run-test.sh | 69 ++ e2e-tests/discovery/run-tests.sh | 8 + e2e-tests/run-tests.sh | 7 + storage/engine_test.go | 2 +- ...3_discoveryservice_client_registration.sql | 20 + vcr/holder/wallet.go | 3 + vcr/holder/wallet_test.go | 5 + vcr/signature/proof/jsonld.go | 3 + 31 files changed, 1905 insertions(+), 252 deletions(-) create mode 100644 discovery/client.go create mode 100644 discovery/client_test.go create mode 100644 discovery/config_test.go create mode 100644 e2e-tests/discovery/definitions/definition.json create mode 100644 e2e-tests/discovery/docker-compose.yml create mode 100644 e2e-tests/discovery/node-A/nuts.yaml create mode 100644 e2e-tests/discovery/node-B/nuts.yaml create mode 100755 e2e-tests/discovery/run-test.sh create mode 100755 e2e-tests/discovery/run-tests.sh create mode 100644 storage/sql_migrations/003_discoveryservice_client_registration.sql diff --git a/README.rst b/README.rst index 55404b0c7d..19e035bf30 100644 --- a/README.rst +++ b/README.rst @@ -176,94 +176,95 @@ The following options can be configured on the server: :widths: 20 30 50 :class: options-table - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ - Key Default Description - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ - configfile 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. - 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. - url Public facing URL of the server (required). Must be HTTPS when strictmode is set. - verbosity info Log level (trace, debug, info, warn, error) - tls.certfile PEM file containing the certificate for the server (also used as client certificate). - tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. - tls.certkeyfile PEM file containing the private key of the server certificate. - tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. - tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. + ============================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ + Key Default Description + ============================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ + configfile 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. + 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. + url Public facing URL of the server (required). Must be HTTPS when strictmode is set. + verbosity info Log level (trace, debug, info, warn, error) + tls.certfile PEM file containing the certificate for the server (also used as client certificate). + tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. + tls.certkeyfile PEM file containing the private key of the server certificate. + tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. + tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. **Auth** - auth.accesstokenlifespan 60 defines how long (in seconds) an access token is valid. Uses default in strict mode. - auth.clockskew 5000 allowed JWT Clock skew in milliseconds - auth.contractvalidators [irma,uzi,dummy,employeeid] sets the different contract validators to use - auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client - auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. - auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. + auth.accesstokenlifespan 60 defines how long (in seconds) an access token is valid. Uses default in strict mode. + auth.clockskew 5000 allowed JWT Clock skew in milliseconds + auth.contractvalidators [irma,uzi,dummy,employeeid] sets the different contract validators to use + auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client + auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. + auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. **Crypto** - crypto.storage fs Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). - crypto.external.address Address of the external storage service. - crypto.external.timeout 100ms Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). - crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. - crypto.vault.pathprefix kv The Vault path prefix. - crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). - crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. + crypto.storage fs Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). + crypto.external.address Address of the external storage service. + crypto.external.timeout 100ms Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). + crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. + crypto.vault.pathprefix kv The Vault path prefix. + crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). + crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. **Discovery** - discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. + discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh its registrations on Discovery Services. Note that it only refreshes registrations that expire soon. + discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. **Events** - events.nats.hostname 0.0.0.0 Hostname for the NATS server - events.nats.port 4222 Port where the NATS server listens on - events.nats.storagedir Directory where file-backed streams are stored in the NATS server - events.nats.timeout 30 Timeout for NATS server operations + events.nats.hostname 0.0.0.0 Hostname for the NATS server + events.nats.port 4222 Port where the NATS server listens on + events.nats.storagedir Directory where file-backed streams are stored in the NATS server + events.nats.timeout 30 Timeout for NATS server operations **GoldenHammer** - goldenhammer.enabled true Whether to enable automatically fixing DID documents with the required endpoints. - goldenhammer.interval 10m0s The interval in which to check for DID documents to fix. + goldenhammer.enabled true Whether to enable automatically fixing DID documents with the required endpoints. + goldenhammer.interval 10m0s The interval in which to check for DID documents to fix. **HTTP** - http.default.address \:1323 Address and port the server will be listening to - http.default.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). - http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', - http.default.auth.audience Expected audience for JWT tokens (default: hostname) - http.default.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers - http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. - http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. + http.default.address \:1323 Address and port the server will be listening to + http.default.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). + http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', + http.default.auth.audience Expected audience for JWT tokens (default: hostname) + http.default.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers + http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. + http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. **JSONLD** - jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. - jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. + jsonld.contexts.localmapping [https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. **Network** - network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. - network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). - network.enablediscovery true Whether to enable automatic connecting to other nodes. - network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. - network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). - network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). - network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. - network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. - network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). - network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. + network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. + network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). + network.enablediscovery true Whether to enable automatic connecting to other nodes. + network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. + network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). + network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). + network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. + network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. + network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). + network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. **PKI** - pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail - pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true + pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail + pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true **Storage** - storage.bbolt.backup.directory Target directory for BBolt database backups. - storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. - storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. - storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. - storage.redis.password Redis database password. If set, it overrides the username in the connection URL. - storage.redis.username Redis database username. If set, it overrides the username in the connection URL. - storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. - storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. - storage.redis.sentinel.password Password for authenticating to Redis Sentinels. - storage.redis.sentinel.username Username for authenticating to Redis Sentinels. - storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis 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'). + storage.bbolt.backup.directory Target directory for BBolt database backups. + storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. + storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. + storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. + storage.redis.password Redis database password. If set, it overrides the username in the connection URL. + storage.redis.username Redis database username. If set, it overrides the username in the connection URL. + storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. + storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. + storage.redis.sentinel.password Password for authenticating to Redis Sentinels. + storage.redis.sentinel.username Username for authenticating to Redis Sentinels. + storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis 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'). **VCR** - vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice). - vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI. - vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations. + vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice). + vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI. + vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations. **policy** - policy.address The address of a remote policy server. Mutual exclusive with policy.directory. - policy.directory Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. Mutual exclusive with policy.address. - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ + policy.address The address of a remote policy server. Mutual exclusive with policy.directory. + policy.directory Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. Mutual exclusive with policy.address. + ============================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ This table is automatically generated using the configuration flags in the core and engines. When they're changed the options table must be regenerated using the Makefile: diff --git a/cmd/root.go b/cmd/root.go index 6700da48c1..51ae167281 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -224,7 +224,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, vdrInstance, storageInstance, policyInstance)) system.RegisterRoutes(&authMeansAPI.Wrapper{Auth: authInstance}) system.RegisterRoutes(&didmanAPI.Wrapper{Didman: didmanInstance}) - system.RegisterRoutes(&discoveryAPI.Wrapper{Server: discoveryInstance}) + system.RegisterRoutes(&discoveryAPI.Wrapper{Server: discoveryInstance, Client: discoveryInstance}) // Register engines // without dependencies diff --git a/discovery/api/v1/generated.go b/discovery/api/v1/generated.go index 67aaee659c..6ef00cc138 100644 --- a/discovery/api/v1/generated.go +++ b/discovery/api/v1/generated.go @@ -110,6 +110,12 @@ type ClientInterface interface { RegisterPresentationWithBody(ctx context.Context, serviceID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) RegisterPresentation(ctx context.Context, serviceID string, body RegisterPresentationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StopRegisteringPresentation request + StopRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StartRegisteringPresentation request + StartRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) GetPresentations(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -148,6 +154,30 @@ func (c *Client) RegisterPresentation(ctx context.Context, serviceID string, bod return c.Client.Do(req) } +func (c *Client) StopRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStopRegisteringPresentationRequest(c.Server, serviceID, did) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StartRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStartRegisteringPresentationRequest(c.Server, serviceID, did) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + // NewGetPresentationsRequest generates requests for GetPresentations func NewGetPresentationsRequest(server string, serviceID string, params *GetPresentationsParams) (*http.Request, error) { var err error @@ -251,6 +281,88 @@ func NewRegisterPresentationRequestWithBody(server string, serviceID string, con return req, nil } +// NewStopRegisteringPresentationRequest generates requests for StopRegisteringPresentation +func NewStopRegisteringPresentationRequest(server string, serviceID string, did string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, serviceID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "did", runtime.ParamLocationPath, did) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/internal/discovery/v1/%s/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewStartRegisteringPresentationRequest generates requests for StartRegisteringPresentation +func NewStartRegisteringPresentationRequest(server string, serviceID string, did string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, serviceID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "did", runtime.ParamLocationPath, did) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/internal/discovery/v1/%s/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { for _, r := range c.RequestEditors { if err := r(ctx, req); err != nil { @@ -301,6 +413,12 @@ type ClientWithResponsesInterface interface { RegisterPresentationWithBodyWithResponse(ctx context.Context, serviceID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterPresentationResponse, error) RegisterPresentationWithResponse(ctx context.Context, serviceID string, body RegisterPresentationJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterPresentationResponse, error) + + // StopRegisteringPresentationWithResponse request + StopRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StopRegisteringPresentationResponse, error) + + // StartRegisteringPresentationWithResponse request + StartRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StartRegisteringPresentationResponse, error) } type GetPresentationsResponse struct { @@ -376,6 +494,96 @@ func (r RegisterPresentationResponse) StatusCode() int { return 0 } +type StopRegisteringPresentationResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *struct { + // Reason Description of why registration deletion failed. + Reason string `json:"reason"` + } + ApplicationproblemJSON400 *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"` + } + ApplicationproblemJSONDefault *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"` + } +} + +// Status returns HTTPResponse.Status +func (r StopRegisteringPresentationResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StopRegisteringPresentationResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type StartRegisteringPresentationResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *struct { + // Reason Description of why registration failed. + Reason string `json:"reason"` + } + ApplicationproblemJSON400 *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"` + } + ApplicationproblemJSONDefault *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"` + } +} + +// Status returns HTTPResponse.Status +func (r StartRegisteringPresentationResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StartRegisteringPresentationResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + // GetPresentationsWithResponse request returning *GetPresentationsResponse func (c *ClientWithResponses) GetPresentationsWithResponse(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*GetPresentationsResponse, error) { rsp, err := c.GetPresentations(ctx, serviceID, params, reqEditors...) @@ -402,6 +610,24 @@ func (c *ClientWithResponses) RegisterPresentationWithResponse(ctx context.Conte return ParseRegisterPresentationResponse(rsp) } +// StopRegisteringPresentationWithResponse request returning *StopRegisteringPresentationResponse +func (c *ClientWithResponses) StopRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StopRegisteringPresentationResponse, error) { + rsp, err := c.StopRegisteringPresentation(ctx, serviceID, did, reqEditors...) + if err != nil { + return nil, err + } + return ParseStopRegisteringPresentationResponse(rsp) +} + +// StartRegisteringPresentationWithResponse request returning *StartRegisteringPresentationResponse +func (c *ClientWithResponses) StartRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StartRegisteringPresentationResponse, error) { + rsp, err := c.StartRegisteringPresentation(ctx, serviceID, did, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartRegisteringPresentationResponse(rsp) +} + // ParseGetPresentationsResponse parses an HTTP response from a GetPresentationsWithResponse call func ParseGetPresentationsResponse(rsp *http.Response) (*GetPresentationsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -495,6 +721,128 @@ func ParseRegisterPresentationResponse(rsp *http.Response) (*RegisterPresentatio return response, nil } +// ParseStopRegisteringPresentationResponse parses an HTTP response from a StopRegisteringPresentationWithResponse call +func ParseStopRegisteringPresentationResponse(rsp *http.Response) (*StopRegisteringPresentationResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &StopRegisteringPresentationResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest struct { + // Reason Description of why registration deletion failed. + Reason string `json:"reason"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest 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"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationproblemJSON400 = &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. + 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"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationproblemJSONDefault = &dest + + } + + return response, nil +} + +// ParseStartRegisteringPresentationResponse parses an HTTP response from a StartRegisteringPresentationWithResponse call +func ParseStartRegisteringPresentationResponse(rsp *http.Response) (*StartRegisteringPresentationResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &StartRegisteringPresentationResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest struct { + // Reason Description of why registration failed. + Reason string `json:"reason"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest 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"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationproblemJSON400 = &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. + 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"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationproblemJSONDefault = &dest + + } + + return response, nil +} + // ServerInterface represents all server handlers. type ServerInterface interface { // Retrieves the presentations of a discovery service. @@ -503,6 +851,12 @@ type ServerInterface interface { // Register a presentation on the discovery service. // (POST /discovery/{serviceID}) RegisterPresentation(ctx echo.Context, serviceID string) error + // Client API to stop registering the given DID on the discovery service. + // (DELETE /internal/discovery/v1/{serviceID}/{did}) + StopRegisteringPresentation(ctx echo.Context, serviceID string, did string) error + // Client API to start registering the given DID on the discovery service. + // (POST /internal/discovery/v1/{serviceID}/{did}) + StartRegisteringPresentation(ctx echo.Context, serviceID string, did string) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -555,6 +909,58 @@ func (w *ServerInterfaceWrapper) RegisterPresentation(ctx echo.Context) error { return err } +// StopRegisteringPresentation converts echo context to params. +func (w *ServerInterfaceWrapper) StopRegisteringPresentation(ctx echo.Context) error { + var err error + // ------------- Path parameter "serviceID" ------------- + var serviceID string + + err = runtime.BindStyledParameterWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, ctx.Param("serviceID"), &serviceID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter serviceID: %s", err)) + } + + // ------------- Path parameter "did" ------------- + var did string + + err = runtime.BindStyledParameterWithLocation("simple", false, "did", runtime.ParamLocationPath, ctx.Param("did"), &did) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter did: %s", err)) + } + + ctx.Set(JwtBearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.StopRegisteringPresentation(ctx, serviceID, did) + return err +} + +// StartRegisteringPresentation converts echo context to params. +func (w *ServerInterfaceWrapper) StartRegisteringPresentation(ctx echo.Context) error { + var err error + // ------------- Path parameter "serviceID" ------------- + var serviceID string + + err = runtime.BindStyledParameterWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, ctx.Param("serviceID"), &serviceID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter serviceID: %s", err)) + } + + // ------------- Path parameter "did" ------------- + var did string + + err = runtime.BindStyledParameterWithLocation("simple", false, "did", runtime.ParamLocationPath, ctx.Param("did"), &did) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter did: %s", err)) + } + + ctx.Set(JwtBearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.StartRegisteringPresentation(ctx, serviceID, did) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -585,6 +991,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/discovery/:serviceID", wrapper.GetPresentations) router.POST(baseURL+"/discovery/:serviceID", wrapper.RegisterPresentation) + router.DELETE(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.StopRegisteringPresentation) + router.POST(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.StartRegisteringPresentation) } @@ -683,6 +1091,142 @@ func (response RegisterPresentationdefaultApplicationProblemPlusJSONResponse) Vi return json.NewEncoder(w).Encode(response.Body) } +type StopRegisteringPresentationRequestObject struct { + ServiceID string `json:"serviceID"` + Did string `json:"did"` +} + +type StopRegisteringPresentationResponseObject interface { + VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error +} + +type StopRegisteringPresentation200Response struct { +} + +func (response StopRegisteringPresentation200Response) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type StopRegisteringPresentation202JSONResponse struct { + // Reason Description of why registration deletion failed. + Reason string `json:"reason"` +} + +func (response StopRegisteringPresentation202JSONResponse) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(202) + + return json.NewEncoder(w).Encode(response) +} + +type StopRegisteringPresentation400ApplicationProblemPlusJSONResponse 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 StopRegisteringPresentation400ApplicationProblemPlusJSONResponse) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type StopRegisteringPresentationdefaultApplicationProblemPlusJSONResponse 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"` + } + StatusCode int +} + +func (response StopRegisteringPresentationdefaultApplicationProblemPlusJSONResponse) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type StartRegisteringPresentationRequestObject struct { + ServiceID string `json:"serviceID"` + Did string `json:"did"` +} + +type StartRegisteringPresentationResponseObject interface { + VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error +} + +type StartRegisteringPresentation200Response struct { +} + +func (response StartRegisteringPresentation200Response) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type StartRegisteringPresentation202JSONResponse struct { + // Reason Description of why registration failed. + Reason string `json:"reason"` +} + +func (response StartRegisteringPresentation202JSONResponse) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(202) + + return json.NewEncoder(w).Encode(response) +} + +type StartRegisteringPresentation400ApplicationProblemPlusJSONResponse 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 StartRegisteringPresentation400ApplicationProblemPlusJSONResponse) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type StartRegisteringPresentationdefaultApplicationProblemPlusJSONResponse 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"` + } + StatusCode int +} + +func (response StartRegisteringPresentationdefaultApplicationProblemPlusJSONResponse) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // Retrieves the presentations of a discovery service. @@ -691,6 +1235,12 @@ type StrictServerInterface interface { // Register a presentation on the discovery service. // (POST /discovery/{serviceID}) RegisterPresentation(ctx context.Context, request RegisterPresentationRequestObject) (RegisterPresentationResponseObject, error) + // Client API to stop registering the given DID on the discovery service. + // (DELETE /internal/discovery/v1/{serviceID}/{did}) + StopRegisteringPresentation(ctx context.Context, request StopRegisteringPresentationRequestObject) (StopRegisteringPresentationResponseObject, error) + // Client API to start registering the given DID on the discovery service. + // (POST /internal/discovery/v1/{serviceID}/{did}) + StartRegisteringPresentation(ctx context.Context, request StartRegisteringPresentationRequestObject) (StartRegisteringPresentationResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -761,3 +1311,55 @@ func (sh *strictHandler) RegisterPresentation(ctx echo.Context, serviceID string } return nil } + +// StopRegisteringPresentation operation middleware +func (sh *strictHandler) StopRegisteringPresentation(ctx echo.Context, serviceID string, did string) error { + var request StopRegisteringPresentationRequestObject + + request.ServiceID = serviceID + request.Did = did + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.StopRegisteringPresentation(ctx.Request().Context(), request.(StopRegisteringPresentationRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StopRegisteringPresentation") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(StopRegisteringPresentationResponseObject); ok { + return validResponse.VisitStopRegisteringPresentationResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// StartRegisteringPresentation operation middleware +func (sh *strictHandler) StartRegisteringPresentation(ctx echo.Context, serviceID string, did string) error { + var request StartRegisteringPresentationRequestObject + + request.ServiceID = serviceID + request.Did = did + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.StartRegisteringPresentation(ctx.Request().Context(), request.(StartRegisteringPresentationRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StartRegisteringPresentation") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(StartRegisteringPresentationResponseObject); ok { + return validResponse.VisitStartRegisteringPresentationResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} diff --git a/discovery/api/v1/wrapper.go b/discovery/api/v1/wrapper.go index 93f58cf394..be79288356 100644 --- a/discovery/api/v1/wrapper.go +++ b/discovery/api/v1/wrapper.go @@ -22,6 +22,8 @@ import ( "context" "errors" "github.com/labstack/echo/v4" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/discovery" "net/http" @@ -56,6 +58,9 @@ func (w *Wrapper) Routes(router core.EchoRouter) { return f(ctx, request) } }, + func(f StrictHandlerFunc, operationID string) StrictHandlerFunc { + return audit.StrictMiddleware(f, discovery.ModuleName, operationID) + }, })) } @@ -83,3 +88,34 @@ func (w *Wrapper) RegisterPresentation(_ context.Context, request RegisterPresen } return RegisterPresentation201Response{}, nil } + +func (w *Wrapper) StartRegisteringPresentation(ctx context.Context, request StartRegisteringPresentationRequestObject) (StartRegisteringPresentationResponseObject, error) { + subjectDID, err := did.ParseDID(request.Did) + if err != nil { + return nil, err + } + err = w.Client.StartRegistration(ctx, request.ServiceID, *subjectDID) + if errors.Is(err, discovery.ErrRegistrationFailed) { + // registration failed, but will be retried + return StartRegisteringPresentation202JSONResponse{ + Reason: err.Error(), + }, nil + } + if err != nil { + // other error + return nil, err + } + return StartRegisteringPresentation200Response{}, nil +} + +func (w *Wrapper) StopRegisteringPresentation(ctx context.Context, request StopRegisteringPresentationRequestObject) (StopRegisteringPresentationResponseObject, error) { + subjectDID, err := did.ParseDID(request.Did) + if err != nil { + return nil, err + } + err = w.Client.StopRegistration(ctx, request.ServiceID, *subjectDID) + if err != nil { + return nil, err + } + return StopRegisteringPresentation200Response{}, nil +} diff --git a/discovery/api/v1/wrapper_test.go b/discovery/api/v1/wrapper_test.go index df838920f0..d094443e75 100644 --- a/discovery/api/v1/wrapper_test.go +++ b/discovery/api/v1/wrapper_test.go @@ -20,6 +20,7 @@ package v1 import ( "errors" + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/discovery" "github.com/stretchr/testify/assert" @@ -102,6 +103,75 @@ func TestWrapper_RegisterPresentation(t *testing.T) { }) } +func TestWrapper_StartRegisteringPresentation(t *testing.T) { + t.Run("ok", func(t *testing.T) { + test := newMockContext(t) + expectedDID := "did:web:example.com" + test.client.EXPECT().StartRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) + + response, err := test.wrapper.StartRegisteringPresentation(nil, StartRegisteringPresentationRequestObject{ + ServiceID: serviceID, + Did: expectedDID, + }) + + assert.NoError(t, err) + assert.IsType(t, StartRegisteringPresentation200Response{}, response) + }) + t.Run("ok, but registration failed", func(t *testing.T) { + test := newMockContext(t) + expectedDID := "did:web:example.com" + test.client.EXPECT().StartRegistration(gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrRegistrationFailed) + + response, err := test.wrapper.StartRegisteringPresentation(nil, StartRegisteringPresentationRequestObject{ + ServiceID: serviceID, + Did: expectedDID, + }) + + assert.NoError(t, err) + assert.IsType(t, StartRegisteringPresentation202JSONResponse{}, response) + }) + t.Run("other error", func(t *testing.T) { + test := newMockContext(t) + expectedDID := "did:web:example.com" + test.client.EXPECT().StartRegistration(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo")) + + _, err := test.wrapper.StartRegisteringPresentation(nil, StartRegisteringPresentationRequestObject{ + ServiceID: serviceID, + Did: expectedDID, + }) + + assert.Error(t, err) + }) +} + +func TestWrapper_StopRegisteringPresentation(t *testing.T) { + t.Run("ok", func(t *testing.T) { + test := newMockContext(t) + expectedDID := "did:web:example.com" + test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) + + response, err := test.wrapper.StopRegisteringPresentation(nil, StopRegisteringPresentationRequestObject{ + ServiceID: serviceID, + Did: expectedDID, + }) + + assert.NoError(t, err) + assert.IsType(t, StopRegisteringPresentation200Response{}, response) + }) + t.Run("error", func(t *testing.T) { + test := newMockContext(t) + expectedDID := "did:web:example.com" + test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(errors.New("foo")) + + _, err := test.wrapper.StopRegisteringPresentation(nil, StopRegisteringPresentationRequestObject{ + ServiceID: serviceID, + Did: expectedDID, + }) + + assert.Error(t, err) + }) +} + func TestWrapper_ResolveStatusCode(t *testing.T) { expected := map[error]int{ discovery.ErrServerModeDisabled: http.StatusBadRequest, diff --git a/discovery/client.go b/discovery/client.go new file mode 100644 index 0000000000..4bc9a0ecde --- /dev/null +++ b/discovery/client.go @@ -0,0 +1,187 @@ +/* + * 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 discovery + +import ( + "context" + "errors" + "fmt" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/audit" + nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/discovery/api/v1/client" + "github.com/nuts-foundation/nuts-node/discovery/log" + "github.com/nuts-foundation/nuts-node/vcr" + "github.com/nuts-foundation/nuts-node/vcr/holder" + "github.com/nuts-foundation/nuts-node/vcr/signature/proof" + "time" +) + +// registrationManager is responsible for managing registrations on a Discovery Service. +type registrationManager interface { + register(ctx context.Context, serviceID string, subjectDID did.DID) error + unregister(ctx context.Context, serviceID string, subjectDID did.DID) error + refreshRegistrations(ctx context.Context, interval time.Duration) +} + +var _ registrationManager = &scheduledRegistrationManager{} + +type scheduledRegistrationManager struct { + services map[string]ServiceDefinition + store *sqlStore + client client.HTTPClient + vcr vcr.VCR +} + +func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR) *scheduledRegistrationManager { + instance := &scheduledRegistrationManager{ + services: services, + store: store, + client: client, + vcr: vcr, + } + return instance +} + +func (r *scheduledRegistrationManager) register(ctx context.Context, serviceID string, subjectDID did.DID) error { + service, serviceExists := r.services[serviceID] + if !serviceExists { + return ErrServiceNotFound + } + // TODO: When to refresh? For now, we refresh when the registration is about to expire (75% of max age) + registrationRenewal := time.Now().Add(time.Duration(float64(service.PresentationMaxValidity)*0.75) * time.Second) + log.Logger().Debugf("Refreshing registration DID on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) + if err := r.store.updateDIDRegistrationTime(serviceID, subjectDID, ®istrationRenewal); err != nil { + return fmt.Errorf("unable to update DID registration: %w", err) + } + err := r.registerPresentation(ctx, subjectDID, service) + if err != nil { + // retry registration asap + var next time.Time + _ = r.store.updateDIDRegistrationTime(serviceID, subjectDID, &next) + return errors.Join(ErrRegistrationFailed, err) + } + log.Logger().Debugf("Successfully refreshed registration DID on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) + return nil +} + +func (r *scheduledRegistrationManager) unregister(ctx context.Context, serviceID string, subjectDID did.DID) error { + // delete DID/service combination from DB, so it won't be registered again + err := r.store.updateDIDRegistrationTime(serviceID, subjectDID, nil) + if err != nil { + return err + } + + // if the DID has an active registration, retract it + presentations, err := r.store.search(serviceID, map[string]string{ + "credentialSubject.id": subjectDID.String(), + }) + if err != nil { + return errors.Join(ErrRegistrationFailed, err) + } + if len(presentations) == 0 { + return nil + } + service := r.services[serviceID] + presentation, err := r.buildPresentation(ctx, subjectDID, service, nil, map[string]interface{}{ + "retract_jti": presentations[0].ID.String(), + }) + if err != nil { + return errors.Join(ErrRegistrationFailed, err) + } + err = r.client.Register(ctx, service.Endpoint, *presentation) + if err != nil { + return errors.Join(ErrRegistrationFailed, err) + } + return nil +} + +func (r *scheduledRegistrationManager) registerPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) error { + presentation, err := r.findCredentialsAndBuildPresentation(ctx, subjectDID, service) + if err != nil { + return err + } + return r.client.Register(ctx, service.Endpoint, *presentation) +} + +func (r *scheduledRegistrationManager) findCredentialsAndBuildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) (*vc.VerifiablePresentation, error) { + credentials, err := r.vcr.Wallet().List(ctx, subjectDID) + if err != nil { + return nil, err + } + matchingCredentials, _, err := service.PresentationDefinition.Match(credentials) + if err != nil { + return nil, fmt.Errorf("failed to match Discovery Service's Presentation Definition (service=%s, did=%s): %w", service.ID, subjectDID, err) + } + if len(matchingCredentials) == 0 { + return nil, fmt.Errorf("DID wallet does not have credentials required for registration on Discovery Service (service=%s, did=%s)", service.ID, subjectDID) + } + return r.buildPresentation(ctx, subjectDID, service, matchingCredentials, nil) +} + +func (r *scheduledRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, + credentials []vc.VerifiableCredential, additionalProperties map[string]interface{}) (*vc.VerifiablePresentation, error) { + nonce := nutsCrypto.GenerateNonce() + expires := time.Now().Add(time.Duration(service.PresentationMaxValidity-1) * time.Second).Truncate(time.Second) + return r.vcr.Wallet().BuildPresentation(ctx, credentials, holder.PresentationOptions{ + ProofOptions: proof.ProofOptions{ + Created: time.Now(), + Domain: &service.ID, + Expires: &expires, + Nonce: &nonce, + AdditionalProperties: additionalProperties, + }, + Format: vc.JWTPresentationProofFormat, + }, &subjectDID, false) +} + +func (r *scheduledRegistrationManager) doRefreshRegistrations(ctx context.Context, now time.Time) error { + log.Logger().Debug("Renewing DID registrations on Discovery Services") + serviceIDs, dids, err := r.store.getStaleDIDRegistrations(now) + if err != nil { + return err + } + for i, serviceID := range serviceIDs { + if err := r.register(ctx, serviceID, dids[i]); err != nil { + log.Logger().WithError(err).Warnf("Failed to renew DID registration (service=%s, did=%s)", serviceID, dids[i]) + } + } + return nil +} + +func (r *scheduledRegistrationManager) refreshRegistrations(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + // do the first refresh immediately + do := func() { + if err := r.doRefreshRegistrations(audit.Context(ctx, "app", ModuleName, "RefreshRegistration"), time.Now()); err != nil { + log.Logger().WithError(err).Errorf("Failed to renew DID registrations") + } + } + do() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + do() + } + } +} diff --git a/discovery/client_test.go b/discovery/client_test.go new file mode 100644 index 0000000000..2b9df22ba3 --- /dev/null +++ b/discovery/client_test.go @@ -0,0 +1,205 @@ +package discovery + +import ( + "context" + "errors" + "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/audit" + "github.com/nuts-foundation/nuts-node/discovery/api/v1/client" + "github.com/nuts-foundation/nuts-node/storage" + "github.com/nuts-foundation/nuts-node/vcr" + "github.com/nuts-foundation/nuts-node/vcr/holder" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "sync" + "testing" + "time" +) + +func Test_scheduledRegistrationManager_register(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) + mockVCR := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) + wallet.EXPECT().BuildPresentation(gomock.Any(), []vc.VerifiableCredential{vcAlice}, gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + + err := manager.register(audit.TestContext(), testServiceID, aliceDID) + + require.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")) + mockVCR := vcr.NewMockVCR(ctrl) + 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.EXPECT().Wallet().Return(wallet).AnyTimes() + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + + err := manager.register(audit.TestContext(), testServiceID, aliceDID) + + require.ErrorIs(t, err, ErrRegistrationFailed) + require.ErrorContains(t, err, "invoker error") + }) + t.Run("no matching credentials", func(t *testing.T) { + ctrl := gomock.NewController(t) + invoker := client.NewMockHTTPClient(ctrl) + mockVCR := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) + mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + + err := manager.register(audit.TestContext(), testServiceID, aliceDID) + + require.ErrorIs(t, err, ErrRegistrationFailed) + require.ErrorContains(t, err, "DID wallet does not have credentials required for registration on Discovery Service (service=usecase_v1, did=did:example:alice)") + }) + 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) + + err := manager.register(audit.TestContext(), "unknown", aliceDID) + + require.EqualError(t, err, "discovery service not found") + }) +} + +func Test_scheduledRegistrationManager_unregister(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(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) + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + + err := manager.unregister(audit.TestContext(), testServiceID, aliceDID) + + 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()) + mockVCR := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + tag := Tag("taggy") + require.NoError(t, store.add(testServiceID, vpAlice, &tag)) + + err := manager.unregister(audit.TestContext(), testServiceID, aliceDID) + + assert.NoError(t, err) + }) + t.Run("unregistering 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")) + mockVCR := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + tag := Tag("taggy") + require.NoError(t, store.add(testServiceID, vpAlice, &tag)) + + err := manager.unregister(audit.TestContext(), testServiceID, aliceDID) + + require.ErrorIs(t, err, ErrRegistrationFailed) + require.ErrorContains(t, err, "remote error") + }) +} + +func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(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) + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + + err := manager.doRefreshRegistrations(audit.TestContext(), time.Now()) + + require.NoError(t, err) + }) + t.Run("2 registrations to renew, first one fails, second one succeeds", func(t *testing.T) { + store := setupStore(t, storageEngine.GetSQLDatabase()) + ctrl := gomock.NewController(t) + invoker := client.NewMockHTTPClient(ctrl) + gomock.InOrder( + invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")), + invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), + ) + mockVCR := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + // Alice + _ = store.updateDIDRegistrationTime(testServiceID, aliceDID, &time.Time{}) + 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) + // Bob + _ = store.updateDIDRegistrationTime(testServiceID, bobDID, &time.Time{}) + 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) + + err := manager.doRefreshRegistrations(audit.TestContext(), time.Now()) + + require.NoError(t, err) + }) +} + +func Test_scheduledRegistrationManager_refreshRegistrations(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + + t.Run("context cancel stops the loop", func(t *testing.T) { + store := setupStore(t, storageEngine.GetSQLDatabase()) + ctrl := gomock.NewController(t) + invoker := client.NewMockHTTPClient(ctrl) + mockVCR := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() + manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) + + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + manager.refreshRegistrations(ctx, time.Millisecond) + }() + // make sure the loop has at least once + time.Sleep(5 * time.Millisecond) + // Make sure the function exits when the context is cancelled + cancel() + wg.Wait() + }) +} diff --git a/discovery/cmd/cmd.go b/discovery/cmd/cmd.go index 302f812471..533aa6aaf7 100644 --- a/discovery/cmd/cmd.go +++ b/discovery/cmd/cmd.go @@ -33,5 +33,8 @@ func FlagSet() *pflag.FlagSet { flagSet.StringSlice("discovery.server.definition_ids", defs.Server.DefinitionIDs, "IDs of the Discovery Service Definitions for which to act as server. "+ "If an ID does not map to a loaded service definition, the node will fail to start.") + flagSet.Duration("discovery.client.registration_refresh_interval", defs.Client.RegistrationRefreshInterval, + "Interval at which the client should refresh its registrations on Discovery Services. "+ + "Note that it only refreshes registrations that expire soon.") return flagSet } diff --git a/discovery/config.go b/discovery/config.go index 1a432cbbf6..15d3cc7d56 100644 --- a/discovery/config.go +++ b/discovery/config.go @@ -18,9 +18,12 @@ package discovery +import "time" + // Config holds the config of the module type Config struct { Server ServerConfig `koanf:"server"` + Client ClientConfig `koanf:"client"` Definitions ServiceDefinitionsConfig `koanf:"definitions"` } @@ -35,14 +38,18 @@ type ServerConfig struct { DefinitionIDs []string `koanf:"definition_ids"` } +// ClientConfig holds the config for the client +type ClientConfig struct { + // RegistrationRefreshInterval specifies how often the client should refresh its registrations on Discovery Services. + RegistrationRefreshInterval time.Duration `koanf:"registration_refresh_interval"` +} + // DefaultConfig returns the default configuration. func DefaultConfig() Config { return Config{ Server: ServerConfig{}, + Client: ClientConfig{ + RegistrationRefreshInterval: 10 * time.Minute, + }, } } - -// IsServer returns true if the node act as Discovery Server. -func (c Config) IsServer() bool { - return len(c.Server.DefinitionIDs) > 0 -} diff --git a/discovery/config_test.go b/discovery/config_test.go new file mode 100644 index 0000000000..689d7816d4 --- /dev/null +++ b/discovery/config_test.go @@ -0,0 +1,10 @@ +package discovery + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDefaultConfig(t *testing.T) { + assert.NotEmpty(t, DefaultConfig().Client.RegistrationRefreshInterval) +} diff --git a/discovery/interface.go b/discovery/interface.go index 0e78892306..4ff38eb27f 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -19,7 +19,9 @@ package discovery import ( + "context" "errors" + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "math" "strconv" @@ -83,6 +85,9 @@ var ErrServiceNotFound = errors.New("discovery service not found") // but a presentation with this ID already exists. var ErrPresentationAlreadyExists = errors.New("presentation already exists") +// ErrRegistrationFailed indicates registration of a presentation on a remote Discovery Service failed. +var ErrRegistrationFailed = errors.New("registration failed") + // Server defines the API for Discovery Servers. type Server interface { // Add registers a presentation on the given Discovery Service. @@ -95,4 +100,16 @@ type Server interface { // Client defines the API for Discovery Clients. type Client interface { Search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) + + // StartRegistration starts registration of presentations on a Discovery Service for the specified DID. + // Registration will be attempted immediately, and automatically refreshed. + // If initial registration failed, it will return ErrRegistrationFailed, but it will keep retrying. + // If the function is called again for the same service/DID combination, it will try to refresh the registration. + // It returns an error if the service or DID is invalid/unknown. + StartRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error + + // StopRegistration stops (automatic) registration of presentations on a Discovery Service for the specified DID. + // It will also try to delete the existing registration on the Discovery Service, if any. + // It returns an error if the service or DID is invalid/unknown. + StopRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error } diff --git a/discovery/mock.go b/discovery/mock.go index 391979c563..0f91d3a950 100644 --- a/discovery/mock.go +++ b/discovery/mock.go @@ -10,8 +10,10 @@ package discovery import ( + context "context" reflect "reflect" + did "github.com/nuts-foundation/go-did/did" vc "github.com/nuts-foundation/go-did/vc" gomock "go.uber.org/mock/gomock" ) @@ -106,3 +108,31 @@ func (mr *MockClientMockRecorder) Search(serviceID, query any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockClient)(nil).Search), serviceID, query) } + +// StartRegistration mocks base method. +func (m *MockClient) StartRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartRegistration", ctx, serviceID, subjectDID) + ret0, _ := ret[0].(error) + return ret0 +} + +// StartRegistration indicates an expected call of StartRegistration. +func (mr *MockClientMockRecorder) StartRegistration(ctx, serviceID, subjectDID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartRegistration", reflect.TypeOf((*MockClient)(nil).StartRegistration), ctx, serviceID, subjectDID) +} + +// StopRegistration mocks base method. +func (m *MockClient) StopRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopRegistration", ctx, serviceID, subjectDID) + ret0, _ := ret[0].(error) + return ret0 +} + +// StopRegistration indicates an expected call of StopRegistration. +func (mr *MockClientMockRecorder) StopRegistration(ctx, serviceID, subjectDID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopRegistration", reflect.TypeOf((*MockClient)(nil).StopRegistration), ctx, serviceID, subjectDID) +} diff --git a/discovery/module.go b/discovery/module.go index 87fb5ed53c..898d909cf4 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -19,17 +19,22 @@ package discovery import ( + "context" "errors" "fmt" 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/core" + "github.com/nuts-foundation/nuts-node/discovery/api/v1/client" "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" + "github.com/nuts-foundation/nuts-node/vcr/log" "os" "path" "strings" + "sync" "time" ) @@ -57,28 +62,37 @@ var _ core.Injectable = &Module{} var _ core.Runnable = &Module{} var _ core.Configurable = &Module{} var _ Server = &Module{} +var _ Client = &Module{} var retractionPresentationType = ssi.MustParseURI("RetractedVerifiablePresentation") // New creates a new Module. func New(storageInstance storage.Engine, vcrInstance vcr.VCR) *Module { - return &Module{ + m := &Module{ storageInstance: storageInstance, vcrInstance: vcrInstance, } + m.ctx, m.cancel = context.WithCancel(context.Background()) + m.routines = new(sync.WaitGroup) + return m } // Module is the main entry point for discovery services. type Module struct { - config Config - storageInstance storage.Engine - store *sqlStore - serverDefinitions map[string]ServiceDefinition - services map[string]ServiceDefinition - vcrInstance vcr.VCR + config Config + httpClient client.HTTPClient + storageInstance storage.Engine + store *sqlStore + registrationManager registrationManager + serverDefinitions map[string]ServiceDefinition + services map[string]ServiceDefinition + vcrInstance vcr.VCR + ctx context.Context + cancel context.CancelFunc + routines *sync.WaitGroup } -func (m *Module) Configure(_ core.ServerConfig) error { +func (m *Module) Configure(serverConfig core.ServerConfig) error { if m.config.Definitions.Directory == "" { return nil } @@ -99,6 +113,7 @@ func (m *Module) Configure(_ core.ServerConfig) error { } m.serverDefinitions = serverDefinitions } + m.httpClient = client.New(serverConfig.Strictmode, 10*time.Second, nil) return nil } @@ -108,10 +123,18 @@ func (m *Module) Start() error { if err != nil { return err } + m.registrationManager = newRegistrationManager(m.services, m.store, m.httpClient, m.vcrInstance) + m.routines.Add(1) + go func() { + defer m.routines.Done() + m.registrationManager.refreshRegistrations(m.ctx, m.config.Client.RegistrationRefreshInterval) + }() return nil } func (m *Module) Shutdown() error { + m.cancel() + m.routines.Wait() return nil } @@ -231,6 +254,26 @@ func (m *Module) Get(serviceID string, tag *Tag) ([]vc.VerifiablePresentation, * return m.store.get(serviceID, tag) } +func (m *Module) Search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) { + panic("implement me") +} + +func (m *Module) StartRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { + log.Logger().Debugf("Registering on Discovery Service (did=%s, service=%s)", subjectDID, serviceID) + err := m.registrationManager.register(ctx, serviceID, subjectDID) + if errors.Is(err, ErrRegistrationFailed) { + log.Logger().WithError(err).Warnf("Discovery Service registration failed, will be retried later (did=%s,service=%s)", subjectDID, serviceID) + } else { + log.Logger().Infof("Successfully registered Discovery Service (did=%s,service=%s)", subjectDID, serviceID) + } + return err +} + +func (m *Module) StopRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { + log.Logger().Infof("Unregistering from Discovery Service (did=%s, service=%s)", subjectDID, serviceID) + return m.registrationManager.unregister(ctx, serviceID, subjectDID) +} + func loadDefinitions(directory string) (map[string]ServiceDefinition, error) { entries, err := os.ReadDir(directory) if err != nil { diff --git a/discovery/module_test.go b/discovery/module_test.go index 3b54d56700..0f7e6124d9 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -38,7 +38,8 @@ func TestModule_Name(t *testing.T) { } func TestModule_Shutdown(t *testing.T) { - assert.NoError(t, (&Module{}).Shutdown()) + module, _ := setupModule(t, storage.NewTestStorageEngine(t)) + require.NoError(t, module.Shutdown()) } func Test_Module_Add(t *testing.T) { @@ -238,6 +239,7 @@ func setupModule(t *testing.T, storageInstance storage.Engine) (*Module, *verifi mockVCR := vcr.NewMockVCR(ctrl) mockVCR.EXPECT().Verifier().Return(mockVerifier).AnyTimes() m := New(storageInstance, mockVCR) + m.config = DefaultConfig() require.NoError(t, m.Configure(core.ServerConfig{})) m.services = testDefinitions() m.serverDefinitions = map[string]ServiceDefinition{ diff --git a/discovery/store.go b/discovery/store.go index b9ffb1e081..c1aa4581ae 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -21,6 +21,7 @@ package discovery import ( "errors" "fmt" + "github.com/nuts-foundation/go-did/did" "math/rand" "strconv" "strings" @@ -103,6 +104,21 @@ func (l credentialPropertyRecord) TableName() string { return "discovery_credential_prop" } +// didRegistrationRecord is a tab-keeping record for clients to keep track of which DIDs should be registered on which Discovery Services. +type didRegistrationRecord struct { + // ServiceID refers to the entry record in discovery_service + ServiceID string `gorm:"primaryKey"` + // Did is Did that should be registered on the service. + Did string `gorm:"primaryKey"` + // NextRegistration is the timestamp (seconds since Unix epoch) when the registration on the Discovery Service should be refreshed. + NextRegistration int64 +} + +// TableName returns the table name for this DTO. +func (l didRegistrationRecord) TableName() string { + return "discovery_did_registration" +} + type sqlStore struct { db *gorm.DB writeLock sync.Mutex @@ -402,6 +418,40 @@ func (s *sqlStore) removeExpired() (int, error) { return int(result.RowsAffected), nil } +// updateDIDRegistrationTime creates/updates the next registration time for a DID on a Discovery Service. +// If nextRegistration is nil, the entry will be removed from the database. +func (s *sqlStore) updateDIDRegistrationTime(serviceID string, subjectDID did.DID, nextRegistration *time.Time) error { + return s.db.Transaction(func(tx *gorm.DB) error { + if nextRegistration == nil { + // Delete registration + return tx.Delete(&didRegistrationRecord{}, "service_id = ? AND did = ?", serviceID, subjectDID.String()).Error + } + // Create or update it + return tx.Save(didRegistrationRecord{Did: subjectDID.String(), ServiceID: serviceID, NextRegistration: nextRegistration.Unix()}).Error + }) +} + +// getStaleDIDRegistrations returns all DID discovery service registrations that are due for renewal. +// It returns a slice of service IDs and associated DIDs. +func (s *sqlStore) getStaleDIDRegistrations(now time.Time) ([]string, []did.DID, error) { + var rows []didRegistrationRecord + if err := s.db.Find(&rows, "next_registration < ?", now.Unix()).Error; err != nil { + return nil, nil, err + } + var dids []did.DID + var serviceIDs []string + for _, row := range rows { + parsedDID, err := did.ParseDID(row.Did) + if err != nil { + log.Logger().WithError(err).Errorf("Invalid DID in discovery DID registration: %s", row.Did) + continue + } + dids = append(dids, *parsedDID) + serviceIDs = append(serviceIDs, row.ServiceID) + } + return serviceIDs, dids, nil +} + // indexJSONObject indexes a JSON object, resulting in a slice of JSON paths and corresponding string values. // It only traverses JSON objects and only adds string values to the result. func indexJSONObject(target map[string]interface{}, jsonPaths []string, stringValues []string, currentPath string) ([]string, []string) { diff --git a/discovery/store_test.go b/discovery/store_test.go index 938c9eade5..8f35d653f7 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -19,6 +19,7 @@ package discovery import ( + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/storage" "github.com/stretchr/testify/assert" @@ -26,6 +27,7 @@ import ( "gorm.io/gorm" "sync" "testing" + "time" ) func Test_sqlStore_exists(t *testing.T) { @@ -355,6 +357,56 @@ func Test_sqlStore_search(t *testing.T) { }) } +func Test_sqlStore_getStaleDIDRegistrations(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + t.Cleanup(func() { + _ = storageEngine.Shutdown() + }) + + now := time.Now() + t.Run("empty list", func(t *testing.T) { + c := setupStore(t, storageEngine.GetSQLDatabase()) + serviceIDs, dids, err := c.getStaleDIDRegistrations(now) + require.NoError(t, err) + assert.Empty(t, serviceIDs) + assert.Empty(t, dids) + }) + t.Run("1 entry, not stale", func(t *testing.T) { + c := setupStore(t, storageEngine.GetSQLDatabase()) + require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, &now)) + serviceIDs, dids, err := c.getStaleDIDRegistrations(time.Now().Add(-1 * time.Hour)) + require.NoError(t, err) + assert.Empty(t, serviceIDs) + assert.Empty(t, dids) + }) + t.Run("1 entry, stale", func(t *testing.T) { + c := setupStore(t, storageEngine.GetSQLDatabase()) + require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, &now)) + serviceIDs, dids, err := c.getStaleDIDRegistrations(time.Now().Add(time.Hour)) + require.NoError(t, err) + assert.Equal(t, []string{testServiceID}, serviceIDs) + assert.Equal(t, []did.DID{aliceDID}, dids) + }) + t.Run("does not return removed entry", func(t *testing.T) { + c := setupStore(t, storageEngine.GetSQLDatabase()) + require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, &now)) + + // Assert it's there + serviceIDs, dids, err := c.getStaleDIDRegistrations(time.Now().Add(time.Hour)) + require.NoError(t, err) + assert.Equal(t, []string{testServiceID}, serviceIDs) + assert.Equal(t, []did.DID{aliceDID}, dids) + + // Remove it + require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, nil)) + serviceIDs, dids, err = c.getStaleDIDRegistrations(time.Now().Add(time.Hour)) + require.NoError(t, err) + assert.Empty(t, serviceIDs) + assert.Empty(t, dids) + }) +} + func setupStore(t *testing.T, db *gorm.DB) *sqlStore { resetStore(t, db) defs := testDefinitions() diff --git a/docs/_static/discovery/v1.yaml b/docs/_static/discovery/v1.yaml index 2d376315b6..e5d295177d 100644 --- a/docs/_static/discovery/v1.yaml +++ b/docs/_static/discovery/v1.yaml @@ -71,6 +71,87 @@ paths: $ref: "../common/error_response.yaml" default: $ref: "../common/error_response.yaml" + /internal/discovery/v1/{serviceID}/{did}: + parameters: + - name: serviceID + in: path + required: true + schema: + type: string + - name: did + in: path + required: true + schema: + type: string + post: + summary: Client API to start registering the given DID on the discovery service. + description: | + An API provided by the discovery client that will cause the given DID to be registered on the specified discovery service. + Registration will be attempted immediately, and it will be automatically refreshed. + Application only need to call this API once for every service/DID combination, until the registration is explicitly deleted through this API. + + For successful registration on the discovery service, the DID's credential wallet must contain the credentials specified by the discovery service definition. + If initial registration fails this API returns the error indicating what failed, but will retry at a later moment. + Applications can force a retry by calling this API again. + + error returns: + * 400 - incorrect input: invalid/unknown service or DID. + operationId: startRegisteringPresentation + tags: + - discovery + responses: + "200": + description: Registration at discovery service was successful. + "202": + description: Registration at discovery service failed, but will be re-attempted later. + content: + application/json: + schema: + type: object + required: + - reason + properties: + reason: + type: string + description: Description of why registration failed. + "400": + $ref: "../common/error_response.yaml" + default: + $ref: "../common/error_response.yaml" + delete: + summary: Client API to stop registering the given DID on the discovery service. + description: | + An API provided by the discovery client that will cause the given DID to be not to be registered any more on the specified discovery service. + It will try to delete the existing registration at the discovery service, if any. + + error returns: + * 400 - incorrect input: invalid/unknown service or DID. + operationId: stopRegisteringPresentation + tags: + - discovery + responses: + "200": + description: | + Registration at discovery service was successfully stopped, and registration at discovery service was deleted + (if applicable). + "202": + description: | + Registration at discovery service was successfully stopped, but failed to delete registration at discovery service. + Applications might want to retry this API call later, or simply let the presentation expire. + content: + application/json: + schema: + type: object + required: + - reason + properties: + reason: + type: string + description: Description of why registration deletion failed. + "400": + $ref: "../common/error_response.yaml" + default: + $ref: "../common/error_response.yaml" components: schemas: VerifiablePresentation: diff --git a/docs/pages/deployment/cli-reference.rst b/docs/pages/deployment/cli-reference.rst index 48d83080c6..93b42b8595 100755 --- a/docs/pages/deployment/cli-reference.rst +++ b/docs/pages/deployment/cli-reference.rst @@ -13,78 +13,79 @@ The following options apply to the server commands below: :: - --auth.accesstokenlifespan int defines how long (in seconds) an access token is valid. Uses default in strict mode. (default 60) - --auth.clockskew int allowed JWT Clock skew in milliseconds (default 5000) - --auth.contractvalidators strings sets the different contract validators to use (default [irma,uzi,dummy,employeeid]) - --auth.http.timeout int HTTP timeout (in seconds) used by the Auth API HTTP client (default 30) - --auth.irma.autoupdateschemas set if you want automatically update the IRMA schemas every 60 minutes. (default true) - --auth.irma.schememanager string IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. (default "pbdf") - --configfile string Nuts config file (default "nuts.yaml") - --cpuprofile string When set, a CPU profile is written to the given path. Ignored when strictmode is set. - --crypto.external.address string Address of the external storage service. - --crypto.external.timeout duration Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). (default 100ms) - --crypto.storage string Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). (default "fs") - --crypto.vault.address string The Vault address. If set it overwrites the VAULT_ADDR env var. - --crypto.vault.pathprefix string The Vault path prefix. (default "kv") - --crypto.vault.timeout duration Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). (default 5s) - --crypto.vault.token string The Vault token. If set it overwrites the VAULT_TOKEN env var. - --datadir string Directory where the node stores its files. (default "./data") - --discovery.definitions.directory string 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.definition_ids strings IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. - --events.nats.hostname string Hostname for the NATS server (default "0.0.0.0") - --events.nats.port int Port where the NATS server listens on (default 4222) - --events.nats.storagedir string Directory where file-backed streams are stored in the NATS server - --events.nats.timeout int Timeout for NATS server operations (default 30) - --goldenhammer.enabled Whether to enable automatically fixing DID documents with the required endpoints. (default true) - --goldenhammer.interval duration The interval in which to check for DID documents to fix. (default 10m0s) - --http.default.address string Address and port the server will be listening to (default ":1323") - --http.default.auth.audience string Expected audience for JWT tokens (default: hostname) - --http.default.auth.authorizedkeyspath string Path to an authorized_keys file for trusted JWT signers - --http.default.auth.type string Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. - --http.default.cors.origin strings When set, enables CORS from the specified origins on the default HTTP interface. - --http.default.log string 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). (default "metadata") - --http.default.tls string Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', - --internalratelimiter When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. (default true) - --jsonld.contexts.localmapping stringToString This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. (default [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson]) - --jsonld.contexts.remoteallowlist strings In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. (default [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json]) - --loggerformat string Log format (text, json) (default "text") - --network.bootstrapnodes strings List of bootstrap nodes (':') which the node initially connect to. - --network.connectiontimeout int Timeout before an outbound connection attempt times out (in milliseconds). (default 5000) - --network.enablediscovery Whether to enable automatic connecting to other nodes. (default true) - --network.enabletls Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. (default true) - --network.grpcaddr string Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). (default ":5555") - --network.maxbackoff duration Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). (default 24h0m0s) - --network.nodedid string Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. - --network.protocols ints Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. - --network.v2.diagnosticsinterval int Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). (default 5000) - --network.v2.gossipinterval int Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. (default 5000) - --pki.maxupdatefailhours int Maximum number of hours that a denylist update can fail (default 4) - --pki.softfail Do not reject certificates if their revocation status cannot be established when softfail is true (default true) - --policy.address string The address of a remote policy server. Mutual exclusive with policy.directory. - --policy.directory string Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. Mutual exclusive with policy.address. - --storage.bbolt.backup.directory string Target directory for BBolt database backups. - --storage.bbolt.backup.interval duration Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. - --storage.redis.address string Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. - --storage.redis.database string Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. - --storage.redis.password string Redis database password. If set, it overrides the username in the connection URL. - --storage.redis.sentinel.master string Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. - --storage.redis.sentinel.nodes strings Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. - --storage.redis.sentinel.password string Password for authenticating to Redis Sentinels. - --storage.redis.sentinel.username string Username for authenticating to Redis Sentinels. - --storage.redis.tls.truststorefile string PEM file containing the trusted CA certificate(s) for authenticating remote Redis servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). - --storage.redis.username string Redis database username. If set, it overrides the username in the connection URL. - --storage.sql.connection string 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'). - --strictmode When set, insecure settings are forbidden. (default true) - --tls.certfile string PEM file containing the certificate for the server (also used as client certificate). - --tls.certheader string Name of the HTTP header that will contain the client certificate when TLS is offloaded. - --tls.certkeyfile string PEM file containing the private key of the server certificate. - --tls.offload string Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. - --tls.truststorefile string PEM file containing the trusted CA certificates for authenticating remote servers. (default "truststore.pem") - --url string Public facing URL of the server (required). Must be HTTPS when strictmode is set. - --vcr.openid4vci.definitionsdir string Directory with the additional credential definitions the node could issue (experimental, may change without notice). - --vcr.openid4vci.enabled Enable issuing and receiving credentials over OpenID4VCI. (default true) - --vcr.openid4vci.timeout duration Time-out for OpenID4VCI HTTP client operations. (default 30s) - --verbosity string Log level (trace, debug, info, warn, error) (default "info") + --auth.accesstokenlifespan int defines how long (in seconds) an access token is valid. Uses default in strict mode. (default 60) + --auth.clockskew int allowed JWT Clock skew in milliseconds (default 5000) + --auth.contractvalidators strings sets the different contract validators to use (default [irma,uzi,dummy,employeeid]) + --auth.http.timeout int HTTP timeout (in seconds) used by the Auth API HTTP client (default 30) + --auth.irma.autoupdateschemas set if you want automatically update the IRMA schemas every 60 minutes. (default true) + --auth.irma.schememanager string IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. (default "pbdf") + --configfile string Nuts config file (default "nuts.yaml") + --cpuprofile string When set, a CPU profile is written to the given path. Ignored when strictmode is set. + --crypto.external.address string Address of the external storage service. + --crypto.external.timeout duration Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). (default 100ms) + --crypto.storage string Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). (default "fs") + --crypto.vault.address string The Vault address. If set it overwrites the VAULT_ADDR env var. + --crypto.vault.pathprefix string The Vault path prefix. (default "kv") + --crypto.vault.timeout duration Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). (default 5s) + --crypto.vault.token string The Vault token. If set it overwrites the VAULT_TOKEN env var. + --datadir string Directory where the node stores its files. (default "./data") + --discovery.client.registration_refresh_interval duration Interval at which the client should refresh its registrations on Discovery Services. Note that it only refreshes registrations that expire soon. (default 10m0s) + --discovery.definitions.directory string 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.definition_ids strings IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. + --events.nats.hostname string Hostname for the NATS server (default "0.0.0.0") + --events.nats.port int Port where the NATS server listens on (default 4222) + --events.nats.storagedir string Directory where file-backed streams are stored in the NATS server + --events.nats.timeout int Timeout for NATS server operations (default 30) + --goldenhammer.enabled Whether to enable automatically fixing DID documents with the required endpoints. (default true) + --goldenhammer.interval duration The interval in which to check for DID documents to fix. (default 10m0s) + --http.default.address string Address and port the server will be listening to (default ":1323") + --http.default.auth.audience string Expected audience for JWT tokens (default: hostname) + --http.default.auth.authorizedkeyspath string Path to an authorized_keys file for trusted JWT signers + --http.default.auth.type string Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. + --http.default.cors.origin strings When set, enables CORS from the specified origins on the default HTTP interface. + --http.default.log string 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). (default "metadata") + --http.default.tls string Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', + --internalratelimiter When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. (default true) + --jsonld.contexts.localmapping stringToString This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. (default [https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson]) + --jsonld.contexts.remoteallowlist strings In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. (default [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json]) + --loggerformat string Log format (text, json) (default "text") + --network.bootstrapnodes strings List of bootstrap nodes (':') which the node initially connect to. + --network.connectiontimeout int Timeout before an outbound connection attempt times out (in milliseconds). (default 5000) + --network.enablediscovery Whether to enable automatic connecting to other nodes. (default true) + --network.enabletls Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. (default true) + --network.grpcaddr string Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). (default ":5555") + --network.maxbackoff duration Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). (default 24h0m0s) + --network.nodedid string Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. + --network.protocols ints Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. + --network.v2.diagnosticsinterval int Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). (default 5000) + --network.v2.gossipinterval int Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. (default 5000) + --pki.maxupdatefailhours int Maximum number of hours that a denylist update can fail (default 4) + --pki.softfail Do not reject certificates if their revocation status cannot be established when softfail is true (default true) + --policy.address string The address of a remote policy server. Mutual exclusive with policy.directory. + --policy.directory string Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. Mutual exclusive with policy.address. + --storage.bbolt.backup.directory string Target directory for BBolt database backups. + --storage.bbolt.backup.interval duration Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. + --storage.redis.address string Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. + --storage.redis.database string Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. + --storage.redis.password string Redis database password. If set, it overrides the username in the connection URL. + --storage.redis.sentinel.master string Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. + --storage.redis.sentinel.nodes strings Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. + --storage.redis.sentinel.password string Password for authenticating to Redis Sentinels. + --storage.redis.sentinel.username string Username for authenticating to Redis Sentinels. + --storage.redis.tls.truststorefile string PEM file containing the trusted CA certificate(s) for authenticating remote Redis servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). + --storage.redis.username string Redis database username. If set, it overrides the username in the connection URL. + --storage.sql.connection string 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'). + --strictmode When set, insecure settings are forbidden. (default true) + --tls.certfile string PEM file containing the certificate for the server (also used as client certificate). + --tls.certheader string Name of the HTTP header that will contain the client certificate when TLS is offloaded. + --tls.certkeyfile string PEM file containing the private key of the server certificate. + --tls.offload string Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. + --tls.truststorefile string PEM file containing the trusted CA certificates for authenticating remote servers. (default "truststore.pem") + --url string Public facing URL of the server (required). Must be HTTPS when strictmode is set. + --vcr.openid4vci.definitionsdir string Directory with the additional credential definitions the node could issue (experimental, may change without notice). + --vcr.openid4vci.enabled Enable issuing and receiving credentials over OpenID4VCI. (default true) + --vcr.openid4vci.timeout duration Time-out for OpenID4VCI HTTP client operations. (default 30s) + --verbosity string Log level (trace, debug, info, warn, error) (default "info") nuts config ^^^^^^^^^^^ diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 9a6fc972fe..5957cf1972 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -2,91 +2,92 @@ :widths: 20 30 50 :class: options-table - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ - Key Default Description - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ - configfile 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. - 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. - url Public facing URL of the server (required). Must be HTTPS when strictmode is set. - verbosity info Log level (trace, debug, info, warn, error) - tls.certfile PEM file containing the certificate for the server (also used as client certificate). - tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. - tls.certkeyfile PEM file containing the private key of the server certificate. - tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. - tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. - **Auth** - auth.accesstokenlifespan 60 defines how long (in seconds) an access token is valid. Uses default in strict mode. - auth.clockskew 5000 allowed JWT Clock skew in milliseconds - auth.contractvalidators [irma,uzi,dummy,employeeid] sets the different contract validators to use - auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client - auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. - auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. - **Crypto** - crypto.storage fs Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). - crypto.external.address Address of the external storage service. - crypto.external.timeout 100ms Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). - crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. - crypto.vault.pathprefix kv The Vault path prefix. - crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). - crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. - **Discovery** - discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. - **Events** - events.nats.hostname 0.0.0.0 Hostname for the NATS server - events.nats.port 4222 Port where the NATS server listens on - events.nats.storagedir Directory where file-backed streams are stored in the NATS server - events.nats.timeout 30 Timeout for NATS server operations - **GoldenHammer** - goldenhammer.enabled true Whether to enable automatically fixing DID documents with the required endpoints. - goldenhammer.interval 10m0s The interval in which to check for DID documents to fix. - **HTTP** - http.default.address \:1323 Address and port the server will be listening to - http.default.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). - http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', - http.default.auth.audience Expected audience for JWT tokens (default: hostname) - http.default.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers - http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. - http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. - **JSONLD** - jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. - jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. - **Network** - network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. - network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). - network.enablediscovery true Whether to enable automatic connecting to other nodes. - network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. - network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). - network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). - network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. - network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. - network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). - network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. - **PKI** - pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail - pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true - **Storage** - storage.bbolt.backup.directory Target directory for BBolt database backups. - storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. - storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. - storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. - storage.redis.password Redis database password. If set, it overrides the username in the connection URL. - storage.redis.username Redis database username. If set, it overrides the username in the connection URL. - storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. - storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. - storage.redis.sentinel.password Password for authenticating to Redis Sentinels. - storage.redis.sentinel.username Username for authenticating to Redis Sentinels. - storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis 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'). - **VCR** - vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice). - vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI. - vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations. - **policy** - policy.address The address of a remote policy server. Mutual exclusive with policy.directory. - policy.directory Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. Mutual exclusive with policy.address. - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ + ============================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ + Key Default Description + ============================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ + configfile 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. + 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. + url Public facing URL of the server (required). Must be HTTPS when strictmode is set. + verbosity info Log level (trace, debug, info, warn, error) + tls.certfile PEM file containing the certificate for the server (also used as client certificate). + tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. + tls.certkeyfile PEM file containing the private key of the server certificate. + tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. + tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. + **Auth** + auth.accesstokenlifespan 60 defines how long (in seconds) an access token is valid. Uses default in strict mode. + auth.clockskew 5000 allowed JWT Clock skew in milliseconds + auth.contractvalidators [irma,uzi,dummy,employeeid] sets the different contract validators to use + auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client + auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. + auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. + **Crypto** + crypto.storage fs Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). + crypto.external.address Address of the external storage service. + crypto.external.timeout 100ms Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). + crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. + crypto.vault.pathprefix kv The Vault path prefix. + crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). + crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. + **Discovery** + discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh its registrations on Discovery Services. Note that it only refreshes registrations that expire soon. + discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. + **Events** + events.nats.hostname 0.0.0.0 Hostname for the NATS server + events.nats.port 4222 Port where the NATS server listens on + events.nats.storagedir Directory where file-backed streams are stored in the NATS server + events.nats.timeout 30 Timeout for NATS server operations + **GoldenHammer** + goldenhammer.enabled true Whether to enable automatically fixing DID documents with the required endpoints. + goldenhammer.interval 10m0s The interval in which to check for DID documents to fix. + **HTTP** + http.default.address \:1323 Address and port the server will be listening to + http.default.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). + http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', + http.default.auth.audience Expected audience for JWT tokens (default: hostname) + http.default.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers + http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. + http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. + **JSONLD** + jsonld.contexts.localmapping [https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. + **Network** + network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. + network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). + network.enablediscovery true Whether to enable automatic connecting to other nodes. + network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. + network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). + network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). + network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. + network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. + network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). + network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. + **PKI** + pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail + pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true + **Storage** + storage.bbolt.backup.directory Target directory for BBolt database backups. + storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. + storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. + storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. + storage.redis.password Redis database password. If set, it overrides the username in the connection URL. + storage.redis.username Redis database username. If set, it overrides the username in the connection URL. + storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. + storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. + storage.redis.sentinel.password Password for authenticating to Redis Sentinels. + storage.redis.sentinel.username Username for authenticating to Redis Sentinels. + storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis 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'). + **VCR** + vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice). + vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI. + vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations. + **policy** + policy.address The address of a remote policy server. Mutual exclusive with policy.directory. + policy.directory Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. Mutual exclusive with policy.address. + ============================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================ diff --git a/e2e-tests/discovery/definitions/definition.json b/e2e-tests/discovery/definitions/definition.json new file mode 100644 index 0000000000..2780260b7a --- /dev/null +++ b/e2e-tests/discovery/definitions/definition.json @@ -0,0 +1,52 @@ +{ + "id": "dev:eOverdracht2023", + "endpoint": "http://nodeA:1323/discovery/dev:eOverdracht2023", + "presentation_max_validity": 2764800, + "presentation_definition": { + "id": "dev:eOverdracht2023", + "format": { + "ldp_vc": { + "proof_type": [ + "JsonWebSignature2020" + ] + }, + "jwt_vp": { + "alg": ["ES256"] + } + }, + "input_descriptors": [ + { + "id": "SelfIssued_NutsOrganizationCredential", + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "string", + "const": "NutsOrganizationCredential" + } + }, + { + "path": [ + "$.credentialSubject.organization.name" + ], + "filter": { + "type": "string" + } + }, + { + "path": [ + "$.credentialSubject.organization.city" + ], + "filter": { + "type": "string" + } + } + ] + } + } + ] + } +} diff --git a/e2e-tests/discovery/docker-compose.yml b/e2e-tests/discovery/docker-compose.yml new file mode 100644 index 0000000000..351917e539 --- /dev/null +++ b/e2e-tests/discovery/docker-compose.yml @@ -0,0 +1,40 @@ +version: "3.7" +services: + nodeA: + image: "${IMAGE_NODE_A:-nutsfoundation/nuts-node:master}" + ports: + - "11323:1323" + - "443" + environment: + NUTS_CONFIGFILE: /opt/nuts/nuts.yaml + volumes: + - "./node-A/nuts.yaml:/opt/nuts/nuts.yaml:ro" + - "./node-A/data:/opt/nuts/data:rw" + - "../tls-certs/nodeA-backend-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" + - "../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" + - "../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro" + # did:web resolver uses the OS CA bundle, but e2e tests use a self-signed CA which can be found in truststore.pem + # So we need to mount that file to the OS CA bundle location, otherwise did:web resolving will fail due to untrusted certs. + - "../tls-certs/truststore.pem:/etc/ssl/certs/Nuts_RootCA.pem:ro" + - "./definitions/:/opt/nuts/definitions:ro" + healthcheck: + interval: 1s # Make test run quicker by checking health status more often + nodeB: + image: "${IMAGE_NODE_B:-nutsfoundation/nuts-node:master}" + ports: + - "21323:1323" + - "443" + environment: + NUTS_CONFIGFILE: /opt/nuts/nuts.yaml + volumes: + - "./node-B/data:/opt/nuts/data:rw" + - "./node-B/nuts.yaml:/opt/nuts/nuts.yaml:ro" + - "../tls-certs/nodeB-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" + - "../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" + - "../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro" + # did:web resolver uses the OS CA bundle, but e2e tests use a self-signed CA which can be found in truststore.pem + # So we need to mount that file to the OS CA bundle location, otherwise did:web resolving will fail due to untrusted certs. + - "../tls-certs/truststore.pem:/etc/ssl/certs/Nuts_RootCA.pem:ro" + - "./definitions/:/opt/nuts/definitions:ro" + healthcheck: + interval: 1s # Make test run quicker by checking health status more often diff --git a/e2e-tests/discovery/node-A/nuts.yaml b/e2e-tests/discovery/node-A/nuts.yaml new file mode 100644 index 0000000000..b0e0ddde74 --- /dev/null +++ b/e2e-tests/discovery/node-A/nuts.yaml @@ -0,0 +1,23 @@ +url: http://nodeA +verbosity: debug +strictmode: false +internalratelimiter: false +datadir: /opt/nuts/data +http: + default: + address: :1323 +discovery: + definitions: + directory: /opt/nuts/definitions + server: + definition_ids: + - dev:eOverdracht2023 +auth: + contractvalidators: + - dummy + irma: + autoupdateschemas: false +tls: + truststorefile: /opt/nuts/truststore.pem + certfile: /opt/nuts/certificate-and-key.pem + certkeyfile: /opt/nuts/certificate-and-key.pem diff --git a/e2e-tests/discovery/node-B/nuts.yaml b/e2e-tests/discovery/node-B/nuts.yaml new file mode 100644 index 0000000000..878515c03b --- /dev/null +++ b/e2e-tests/discovery/node-B/nuts.yaml @@ -0,0 +1,25 @@ +url: https://nodeB +verbosity: debug +strictmode: false +internalratelimiter: false +datadir: /opt/nuts/data +http: + default: + address: :1323 + alt: + iam: + tls: server + address: :443 +discovery: + definitions: + directory: /opt/nuts/definitions +auth: + v2apienabled: true + contractvalidators: + - dummy + irma: + autoupdateschemas: false +tls: + truststorefile: /opt/nuts/truststore.pem + certfile: /opt/nuts/certificate-and-key.pem + certkeyfile: /opt/nuts/certificate-and-key.pem diff --git a/e2e-tests/discovery/run-test.sh b/e2e-tests/discovery/run-test.sh new file mode 100755 index 0000000000..55eacc723a --- /dev/null +++ b/e2e-tests/discovery/run-test.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +source ../util.sh + +# This test asserts the following: +# - Clients update the Discovery Service +# - Clients can register presentations on the Discovery Service +# - When a presentation can't be registered on the Discovery Service, the client will retry +# - Clients can find presentations on the Discovery Service + +echo "------------------------------------" +echo "Cleaning up running Docker containers and volumes, and key material..." +echo "------------------------------------" +docker compose down +docker compose rm -f -v +rm -rf ./node-*/data +mkdir ./node-A/data ./node-B/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 up --wait nodeB + +echo "------------------------------------" +echo "Registering care organization..." +echo "------------------------------------" +DIDDOC=$(docker compose exec nodeB nuts vdr create-did --v2) +DID=$(echo $DIDDOC | jq -r .id) +echo DID: $DID + +REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${DID}\", \"credentialSubject\": {\"id\":\"${DID}\", \"organization\":{\"name\":\"Caresoft B.V.\", \"city\":\"Caretown\"}},\"publishToNetwork\": false}" +RESPONSE=$(echo $REQUEST | curl --insecure -s -X POST --data-binary @- http://localhost:21323/internal/vcr/v2/issuer/vc -H "Content-Type:application/json") +if echo $RESPONSE | grep -q "VerifiableCredential"; then + echo "VC issued" +else + echo "FAILED: Could not issue NutsOrganizationCredential" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + +RESPONSE=$(echo $RESPONSE | curl --insecure -s -X POST --data-binary @- http://localhost:21323/internal/vcr/v2/holder/${DID}/vc -H "Content-Type:application/json") +if [$RESPONSE -eq ""]; then + echo "VC stored in wallet" +else + echo "FAILED: Could not load NutsOrganizationCredential" 1>&2 + echo $RESPONSE + exitWithDockerLogs 1 +fi + +echo "---------------------------------------" +echo "Registering care organization on Discovery Service..." +echo "---------------------------------------" +curl --insecure -s -X POST http://localhost:21323/internal/discovery/v1/dev:eOverdracht2023/${DID} + +echo "---------------------------------------" +echo "Restarting to force registration on Discovery Service..." +echo "---------------------------------------" +docker compose up --wait nodeA +docker compose down nodeB +docker compose up --wait nodeB + +echo "---------------------------------------" +echo "Searching for care organization registration..." +echo "---------------------------------------" +echo "TODO: Requires clients updating discovery service and search API (https://github.com/nuts-foundation/nuts-node/pull/2672)" + +echo "------------------------------------" +echo "Stopping Docker containers..." +echo "------------------------------------" +docker compose stop diff --git a/e2e-tests/discovery/run-tests.sh b/e2e-tests/discovery/run-tests.sh new file mode 100755 index 0000000000..c6f452f2ba --- /dev/null +++ b/e2e-tests/discovery/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: Discovery !!" +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 27db218605..1d110f798f 100755 --- a/e2e-tests/run-tests.sh +++ b/e2e-tests/run-tests.sh @@ -50,3 +50,10 @@ echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" pushd denylist ./run-tests.sh popd + +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "!! Running test suite: Discovery !!" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +pushd discovery +./run-tests.sh +popd diff --git a/storage/engine_test.go b/storage/engine_test.go index aa27b500a1..ab91e89e08 100644 --- a/storage/engine_test.go +++ b/storage/engine_test.go @@ -143,6 +143,6 @@ func Test_engine_sqlDatabase(t *testing.T) { require.NoError(t, row.Err()) var count int assert.NoError(t, row.Scan(&count)) - assert.Equal(t, 2, count) + assert.Equal(t, 3, count) }) } diff --git a/storage/sql_migrations/003_discoveryservice_client_registration.sql b/storage/sql_migrations/003_discoveryservice_client_registration.sql new file mode 100644 index 0000000000..937c39d0b4 --- /dev/null +++ b/storage/sql_migrations/003_discoveryservice_client_registration.sql @@ -0,0 +1,20 @@ +-- migrate:up +-- discovery_did_registration contains the DIDs that should be registered on the specified Discovery Service(s). +create table discovery_did_registration +( + -- service_id is the ID of the Discover Service that the DID should be registered on. + -- It comes from the service definition. + service_id varchar(200) not null, + -- did is the DID that should be registered on the Discovery Service. + did varchar not null, + -- next_registration is the timestamp (seconds since Unix epoch) when the registration on the + -- Discovery Service should be refreshed. + next_registration integer not null, + primary key (service_id, did), + constraint fk_discovery_did_registration_service foreign key (service_id) references discovery_service (id) on delete cascade +); +-- index for the next_registration column, used when checking which registrations need to be refreshed +create index idx_discovery_did_registration_refresh on discovery_did_registration (next_registration); + +-- migrate:down +drop table discovery_did_registration; diff --git a/vcr/holder/wallet.go b/vcr/holder/wallet.go index e383235c5d..af03222ada 100644 --- a/vcr/holder/wallet.go +++ b/vcr/holder/wallet.go @@ -138,6 +138,9 @@ func (h wallet) buildJWTPresentation(ctx context.Context, subjectDID did.DID, cr if options.ProofOptions.Expires != nil { claims[jwt.ExpirationKey] = int(options.ProofOptions.Expires.Unix()) } + for claimName, value := range options.ProofOptions.AdditionalProperties { + claims[claimName] = value + } token, err := h.keyStore.SignJWT(ctx, claims, headers, key) if err != nil { return nil, fmt.Errorf("unable to sign JWT presentation: %w", err) diff --git a/vcr/holder/wallet_test.go b/vcr/holder/wallet_test.go index 0401ec82b9..0604267256 100644 --- a/vcr/holder/wallet_test.go +++ b/vcr/holder/wallet_test.go @@ -192,6 +192,9 @@ func TestWallet_BuildPresentation(t *testing.T) { Created: exp.Add(-1 * time.Hour), Domain: &domain, Nonce: &nonce, + AdditionalProperties: map[string]interface{}{ + "custom": "claim", + }, }, } @@ -213,6 +216,8 @@ func TestWallet_BuildPresentation(t *testing.T) { assert.Equal(t, []string{domain}, result.JWT().Audience()) actualNonce, _ := result.JWT().Get("nonce") assert.Equal(t, nonce, actualNonce) + actualCustomClaim, _ := result.JWT().Get("custom") + assert.Equal(t, "claim", actualCustomClaim) }) }) t.Run("validation", func(t *testing.T) { diff --git a/vcr/signature/proof/jsonld.go b/vcr/signature/proof/jsonld.go index f4914b9c00..ccf0d412ff 100644 --- a/vcr/signature/proof/jsonld.go +++ b/vcr/signature/proof/jsonld.go @@ -64,6 +64,9 @@ type ProofOptions struct { ProofPurpose string `json:"proofPurpose"` // Nonce contains a value that is used to prevent replay attacks Nonce *string `json:"nonce,omitempty"` + // AdditionalProperties is used to specify additional, non-standard properties. + // They are included as JWT claims in jwt_vp proof format, all other formats ignore them (for now). + AdditionalProperties map[string]interface{} `json:"-"` } // ValidAt checks if the proof is valid at a certain given time. From 804e80de16ab4b3b6966abf202c45639b455c744 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 9 Jan 2024 11:22:49 +0100 Subject: [PATCH 02/15] docs --- README.rst | 4 ++-- discovery/client.go | 7 +++++++ discovery/cmd/cmd.go | 4 ++-- docs/pages/deployment/cli-reference.rst | 4 ++-- docs/pages/deployment/server_options.rst | 4 ++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 19e035bf30..b7c8ca9f0b 100644 --- a/README.rst +++ b/README.rst @@ -208,7 +208,7 @@ The following options can be configured on the server: crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. **Discovery** - discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh its registrations on Discovery Services. Note that it only refreshes registrations that expire soon. + discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. **Events** @@ -228,7 +228,7 @@ The following options can be configured on the server: http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. **JSONLD** - jsonld.contexts.localmapping [https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. **Network** network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. diff --git a/discovery/client.go b/discovery/client.go index 4bc9a0ecde..de2d708d38 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -35,9 +35,13 @@ import ( ) // registrationManager is responsible for managing registrations on a Discovery Service. +// It automatically refreshes registrations when they are about to expire. type registrationManager interface { register(ctx context.Context, serviceID string, subjectDID did.DID) error unregister(ctx context.Context, serviceID string, subjectDID did.DID) error + // refreshRegistrations is a blocking call to periodically refresh registrations. + // It checks for registrations to be refreshed at the specified interval. + // It will exit when the given context is cancelled. refreshRegistrations(ctx context.Context, interval time.Duration) } @@ -97,8 +101,10 @@ func (r *scheduledRegistrationManager) unregister(ctx context.Context, serviceID return errors.Join(ErrRegistrationFailed, err) } if len(presentations) == 0 { + // no registration, nothing to do return nil } + // found an active registration, try to delete it from the discovery server service := r.services[serviceID] presentation, err := r.buildPresentation(ctx, subjectDID, service, nil, map[string]interface{}{ "retract_jti": presentations[0].ID.String(), @@ -139,6 +145,7 @@ func (r *scheduledRegistrationManager) findCredentialsAndBuildPresentation(ctx c func (r *scheduledRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, credentials []vc.VerifiableCredential, additionalProperties map[string]interface{}) (*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) return r.vcr.Wallet().BuildPresentation(ctx, credentials, holder.PresentationOptions{ ProofOptions: proof.ProofOptions{ diff --git a/discovery/cmd/cmd.go b/discovery/cmd/cmd.go index 533aa6aaf7..b613836a35 100644 --- a/discovery/cmd/cmd.go +++ b/discovery/cmd/cmd.go @@ -34,7 +34,7 @@ func FlagSet() *pflag.FlagSet { "IDs of the Discovery Service Definitions for which to act as server. "+ "If an ID does not map to a loaded service definition, the node will fail to start.") flagSet.Duration("discovery.client.registration_refresh_interval", defs.Client.RegistrationRefreshInterval, - "Interval at which the client should refresh its registrations on Discovery Services. "+ - "Note that it only refreshes registrations that expire soon.") + "Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. "+ + "Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left).") return flagSet } diff --git a/docs/pages/deployment/cli-reference.rst b/docs/pages/deployment/cli-reference.rst index 93b42b8595..3fac03da12 100755 --- a/docs/pages/deployment/cli-reference.rst +++ b/docs/pages/deployment/cli-reference.rst @@ -29,7 +29,7 @@ The following options apply to the server commands below: --crypto.vault.timeout duration Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). (default 5s) --crypto.vault.token string The Vault token. If set it overwrites the VAULT_TOKEN env var. --datadir string Directory where the node stores its files. (default "./data") - --discovery.client.registration_refresh_interval duration Interval at which the client should refresh its registrations on Discovery Services. Note that it only refreshes registrations that expire soon. (default 10m0s) + --discovery.client.registration_refresh_interval duration Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). (default 10m0s) --discovery.definitions.directory string 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.definition_ids strings IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. --events.nats.hostname string Hostname for the NATS server (default "0.0.0.0") @@ -46,7 +46,7 @@ The following options apply to the server commands below: --http.default.log string 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). (default "metadata") --http.default.tls string Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', --internalratelimiter When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. (default true) - --jsonld.contexts.localmapping stringToString This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. (default [https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson]) + --jsonld.contexts.localmapping stringToString This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. (default [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson]) --jsonld.contexts.remoteallowlist strings In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. (default [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json]) --loggerformat string Log format (text, json) (default "text") --network.bootstrapnodes strings List of bootstrap nodes (':') which the node initially connect to. diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 5957cf1972..1729f6fc8f 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -34,7 +34,7 @@ crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. **Discovery** - discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh its registrations on Discovery Services. Note that it only refreshes registrations that expire soon. + discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. **Events** @@ -54,7 +54,7 @@ http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. **JSONLD** - jsonld.contexts.localmapping [https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. **Network** network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. From b03116a15ddce2720aaf6c3f9e0a8b2ecfe7a915 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 9 Jan 2024 13:44:41 +0100 Subject: [PATCH 03/15] PR feedback --- discovery/api/v1/generated.go | 168 +++++++++++++++---------------- discovery/api/v1/wrapper.go | 16 +-- discovery/api/v1/wrapper_test.go | 20 ++-- discovery/client.go | 24 ++--- discovery/client_test.go | 8 +- discovery/interface.go | 9 +- discovery/mock.go | 12 +-- discovery/module.go | 6 +- discovery/store.go | 2 +- docs/_static/discovery/v1.yaml | 46 ++++----- 10 files changed, 155 insertions(+), 156 deletions(-) diff --git a/discovery/api/v1/generated.go b/discovery/api/v1/generated.go index 945ceff38a..43572dfe1e 100644 --- a/discovery/api/v1/generated.go +++ b/discovery/api/v1/generated.go @@ -131,11 +131,11 @@ type ClientInterface interface { // SearchPresentations request SearchPresentations(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - // StopRegisteringPresentation request - StopRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) + // UnregisterDID request + UnregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) - // StartRegisteringPresentation request - StartRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) + // RegisterDID request + RegisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) GetPresentations(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -186,8 +186,8 @@ func (c *Client) SearchPresentations(ctx context.Context, serviceID string, para return c.Client.Do(req) } -func (c *Client) StopRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewStopRegisteringPresentationRequest(c.Server, serviceID, did) +func (c *Client) UnregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUnregisterDIDRequest(c.Server, serviceID, did) if err != nil { return nil, err } @@ -198,8 +198,8 @@ func (c *Client) StopRegisteringPresentation(ctx context.Context, serviceID stri return c.Client.Do(req) } -func (c *Client) StartRegisteringPresentation(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewStartRegisteringPresentationRequest(c.Server, serviceID, did) +func (c *Client) RegisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRegisterDIDRequest(c.Server, serviceID, did) if err != nil { return nil, err } @@ -365,8 +365,8 @@ func NewSearchPresentationsRequest(server string, serviceID string, params *Sear return req, nil } -// NewStopRegisteringPresentationRequest generates requests for StopRegisteringPresentation -func NewStopRegisteringPresentationRequest(server string, serviceID string, did string) (*http.Request, error) { +// NewUnregisterDIDRequest generates requests for UnregisterDID +func NewUnregisterDIDRequest(server string, serviceID string, did string) (*http.Request, error) { var err error var pathParam0 string @@ -406,8 +406,8 @@ func NewStopRegisteringPresentationRequest(server string, serviceID string, did return req, nil } -// NewStartRegisteringPresentationRequest generates requests for StartRegisteringPresentation -func NewStartRegisteringPresentationRequest(server string, serviceID string, did string) (*http.Request, error) { +// NewRegisterDIDRequest generates requests for RegisterDID +func NewRegisterDIDRequest(server string, serviceID string, did string) (*http.Request, error) { var err error var pathParam0 string @@ -501,11 +501,11 @@ type ClientWithResponsesInterface interface { // SearchPresentationsWithResponse request SearchPresentationsWithResponse(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*SearchPresentationsResponse, error) - // StopRegisteringPresentationWithResponse request - StopRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StopRegisteringPresentationResponse, error) + // UnregisterDIDWithResponse request + UnregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*UnregisterDIDResponse, error) - // StartRegisteringPresentationWithResponse request - StartRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StartRegisteringPresentationResponse, error) + // RegisterDIDWithResponse request + RegisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*RegisterDIDResponse, error) } type GetPresentationsResponse struct { @@ -613,7 +613,7 @@ func (r SearchPresentationsResponse) StatusCode() int { return 0 } -type StopRegisteringPresentationResponse struct { +type UnregisterDIDResponse struct { Body []byte HTTPResponse *http.Response JSON202 *struct { @@ -643,7 +643,7 @@ type StopRegisteringPresentationResponse struct { } // Status returns HTTPResponse.Status -func (r StopRegisteringPresentationResponse) Status() string { +func (r UnregisterDIDResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -651,14 +651,14 @@ func (r StopRegisteringPresentationResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r StopRegisteringPresentationResponse) StatusCode() int { +func (r UnregisterDIDResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type StartRegisteringPresentationResponse struct { +type RegisterDIDResponse struct { Body []byte HTTPResponse *http.Response JSON202 *struct { @@ -688,7 +688,7 @@ type StartRegisteringPresentationResponse struct { } // Status returns HTTPResponse.Status -func (r StartRegisteringPresentationResponse) Status() string { +func (r RegisterDIDResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -696,7 +696,7 @@ func (r StartRegisteringPresentationResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r StartRegisteringPresentationResponse) StatusCode() int { +func (r RegisterDIDResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -738,22 +738,22 @@ func (c *ClientWithResponses) SearchPresentationsWithResponse(ctx context.Contex return ParseSearchPresentationsResponse(rsp) } -// StopRegisteringPresentationWithResponse request returning *StopRegisteringPresentationResponse -func (c *ClientWithResponses) StopRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StopRegisteringPresentationResponse, error) { - rsp, err := c.StopRegisteringPresentation(ctx, serviceID, did, reqEditors...) +// UnregisterDIDWithResponse request returning *UnregisterDIDResponse +func (c *ClientWithResponses) UnregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*UnregisterDIDResponse, error) { + rsp, err := c.UnregisterDID(ctx, serviceID, did, reqEditors...) if err != nil { return nil, err } - return ParseStopRegisteringPresentationResponse(rsp) + return ParseUnregisterDIDResponse(rsp) } -// StartRegisteringPresentationWithResponse request returning *StartRegisteringPresentationResponse -func (c *ClientWithResponses) StartRegisteringPresentationWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*StartRegisteringPresentationResponse, error) { - rsp, err := c.StartRegisteringPresentation(ctx, serviceID, did, reqEditors...) +// RegisterDIDWithResponse request returning *RegisterDIDResponse +func (c *ClientWithResponses) RegisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*RegisterDIDResponse, error) { + rsp, err := c.RegisterDID(ctx, serviceID, did, reqEditors...) if err != nil { return nil, err } - return ParseStartRegisteringPresentationResponse(rsp) + return ParseRegisterDIDResponse(rsp) } // ParseGetPresentationsResponse parses an HTTP response from a GetPresentationsWithResponse call @@ -891,15 +891,15 @@ func ParseSearchPresentationsResponse(rsp *http.Response) (*SearchPresentationsR return response, nil } -// ParseStopRegisteringPresentationResponse parses an HTTP response from a StopRegisteringPresentationWithResponse call -func ParseStopRegisteringPresentationResponse(rsp *http.Response) (*StopRegisteringPresentationResponse, error) { +// ParseUnregisterDIDResponse parses an HTTP response from a UnregisterDIDWithResponse call +func ParseUnregisterDIDResponse(rsp *http.Response) (*UnregisterDIDResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StopRegisteringPresentationResponse{ + response := &UnregisterDIDResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -952,15 +952,15 @@ func ParseStopRegisteringPresentationResponse(rsp *http.Response) (*StopRegister return response, nil } -// ParseStartRegisteringPresentationResponse parses an HTTP response from a StartRegisteringPresentationWithResponse call -func ParseStartRegisteringPresentationResponse(rsp *http.Response) (*StartRegisteringPresentationResponse, error) { +// ParseRegisterDIDResponse parses an HTTP response from a RegisterDIDWithResponse call +func ParseRegisterDIDResponse(rsp *http.Response) (*RegisterDIDResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StartRegisteringPresentationResponse{ + response := &RegisterDIDResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -1026,10 +1026,10 @@ type ServerInterface interface { SearchPresentations(ctx echo.Context, serviceID string, params SearchPresentationsParams) error // Client API to stop registering the given DID on the discovery service. // (DELETE /internal/discovery/v1/{serviceID}/{did}) - StopRegisteringPresentation(ctx echo.Context, serviceID string, did string) error + UnregisterDID(ctx echo.Context, serviceID string, did string) error // Client API to start registering the given DID on the discovery service. // (POST /internal/discovery/v1/{serviceID}/{did}) - StartRegisteringPresentation(ctx echo.Context, serviceID string, did string) error + RegisterDID(ctx echo.Context, serviceID string, did string) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -1109,8 +1109,8 @@ func (w *ServerInterfaceWrapper) SearchPresentations(ctx echo.Context) error { return err } -// StopRegisteringPresentation converts echo context to params. -func (w *ServerInterfaceWrapper) StopRegisteringPresentation(ctx echo.Context) error { +// UnregisterDID converts echo context to params. +func (w *ServerInterfaceWrapper) UnregisterDID(ctx echo.Context) error { var err error // ------------- Path parameter "serviceID" ------------- var serviceID string @@ -1131,12 +1131,12 @@ func (w *ServerInterfaceWrapper) StopRegisteringPresentation(ctx echo.Context) e ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.StopRegisteringPresentation(ctx, serviceID, did) + err = w.Handler.UnregisterDID(ctx, serviceID, did) return err } -// StartRegisteringPresentation converts echo context to params. -func (w *ServerInterfaceWrapper) StartRegisteringPresentation(ctx echo.Context) error { +// RegisterDID converts echo context to params. +func (w *ServerInterfaceWrapper) RegisterDID(ctx echo.Context) error { var err error // ------------- Path parameter "serviceID" ------------- var serviceID string @@ -1157,7 +1157,7 @@ func (w *ServerInterfaceWrapper) StartRegisteringPresentation(ctx echo.Context) ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.StartRegisteringPresentation(ctx, serviceID, did) + err = w.Handler.RegisterDID(ctx, serviceID, did) return err } @@ -1192,8 +1192,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/discovery/:serviceID", wrapper.GetPresentations) router.POST(baseURL+"/discovery/:serviceID", wrapper.RegisterPresentation) router.GET(baseURL+"/discovery/:serviceID/search", wrapper.SearchPresentations) - router.DELETE(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.StopRegisteringPresentation) - router.POST(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.StartRegisteringPresentation) + router.DELETE(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.UnregisterDID) + router.POST(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.RegisterDID) } @@ -1331,36 +1331,36 @@ func (response SearchPresentationsdefaultApplicationProblemPlusJSONResponse) Vis return json.NewEncoder(w).Encode(response.Body) } -type StopRegisteringPresentationRequestObject struct { +type UnregisterDIDRequestObject struct { ServiceID string `json:"serviceID"` Did string `json:"did"` } -type StopRegisteringPresentationResponseObject interface { - VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error +type UnregisterDIDResponseObject interface { + VisitUnregisterDIDResponse(w http.ResponseWriter) error } -type StopRegisteringPresentation200Response struct { +type UnregisterDID200Response struct { } -func (response StopRegisteringPresentation200Response) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response UnregisterDID200Response) VisitUnregisterDIDResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type StopRegisteringPresentation202JSONResponse struct { +type UnregisterDID202JSONResponse struct { // Reason Description of why registration deletion failed. Reason string `json:"reason"` } -func (response StopRegisteringPresentation202JSONResponse) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response UnregisterDID202JSONResponse) VisitUnregisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(202) return json.NewEncoder(w).Encode(response) } -type StopRegisteringPresentation400ApplicationProblemPlusJSONResponse struct { +type UnregisterDID400ApplicationProblemPlusJSONResponse struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1371,14 +1371,14 @@ type StopRegisteringPresentation400ApplicationProblemPlusJSONResponse struct { Title string `json:"title"` } -func (response StopRegisteringPresentation400ApplicationProblemPlusJSONResponse) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response UnregisterDID400ApplicationProblemPlusJSONResponse) VisitUnregisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type StopRegisteringPresentationdefaultApplicationProblemPlusJSONResponse struct { +type UnregisterDIDdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1392,43 +1392,43 @@ type StopRegisteringPresentationdefaultApplicationProblemPlusJSONResponse struct StatusCode int } -func (response StopRegisteringPresentationdefaultApplicationProblemPlusJSONResponse) VisitStopRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response UnregisterDIDdefaultApplicationProblemPlusJSONResponse) VisitUnregisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) return json.NewEncoder(w).Encode(response.Body) } -type StartRegisteringPresentationRequestObject struct { +type RegisterDIDRequestObject struct { ServiceID string `json:"serviceID"` Did string `json:"did"` } -type StartRegisteringPresentationResponseObject interface { - VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error +type RegisterDIDResponseObject interface { + VisitRegisterDIDResponse(w http.ResponseWriter) error } -type StartRegisteringPresentation200Response struct { +type RegisterDID200Response struct { } -func (response StartRegisteringPresentation200Response) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response RegisterDID200Response) VisitRegisterDIDResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type StartRegisteringPresentation202JSONResponse struct { +type RegisterDID202JSONResponse struct { // Reason Description of why registration failed. Reason string `json:"reason"` } -func (response StartRegisteringPresentation202JSONResponse) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response RegisterDID202JSONResponse) VisitRegisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(202) return json.NewEncoder(w).Encode(response) } -type StartRegisteringPresentation400ApplicationProblemPlusJSONResponse struct { +type RegisterDID400ApplicationProblemPlusJSONResponse struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1439,14 +1439,14 @@ type StartRegisteringPresentation400ApplicationProblemPlusJSONResponse struct { Title string `json:"title"` } -func (response StartRegisteringPresentation400ApplicationProblemPlusJSONResponse) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response RegisterDID400ApplicationProblemPlusJSONResponse) VisitRegisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type StartRegisteringPresentationdefaultApplicationProblemPlusJSONResponse struct { +type RegisterDIDdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1460,7 +1460,7 @@ type StartRegisteringPresentationdefaultApplicationProblemPlusJSONResponse struc StatusCode int } -func (response StartRegisteringPresentationdefaultApplicationProblemPlusJSONResponse) VisitStartRegisteringPresentationResponse(w http.ResponseWriter) error { +func (response RegisterDIDdefaultApplicationProblemPlusJSONResponse) VisitRegisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) @@ -1480,10 +1480,10 @@ type StrictServerInterface interface { SearchPresentations(ctx context.Context, request SearchPresentationsRequestObject) (SearchPresentationsResponseObject, error) // Client API to stop registering the given DID on the discovery service. // (DELETE /internal/discovery/v1/{serviceID}/{did}) - StopRegisteringPresentation(ctx context.Context, request StopRegisteringPresentationRequestObject) (StopRegisteringPresentationResponseObject, error) + UnregisterDID(ctx context.Context, request UnregisterDIDRequestObject) (UnregisterDIDResponseObject, error) // Client API to start registering the given DID on the discovery service. // (POST /internal/discovery/v1/{serviceID}/{did}) - StartRegisteringPresentation(ctx context.Context, request StartRegisteringPresentationRequestObject) (StartRegisteringPresentationResponseObject, error) + RegisterDID(ctx context.Context, request RegisterDIDRequestObject) (RegisterDIDResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -1581,52 +1581,52 @@ func (sh *strictHandler) SearchPresentations(ctx echo.Context, serviceID string, return nil } -// StopRegisteringPresentation operation middleware -func (sh *strictHandler) StopRegisteringPresentation(ctx echo.Context, serviceID string, did string) error { - var request StopRegisteringPresentationRequestObject +// UnregisterDID operation middleware +func (sh *strictHandler) UnregisterDID(ctx echo.Context, serviceID string, did string) error { + var request UnregisterDIDRequestObject request.ServiceID = serviceID request.Did = did handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.StopRegisteringPresentation(ctx.Request().Context(), request.(StopRegisteringPresentationRequestObject)) + return sh.ssi.UnregisterDID(ctx.Request().Context(), request.(UnregisterDIDRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "StopRegisteringPresentation") + handler = middleware(handler, "UnregisterDID") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(StopRegisteringPresentationResponseObject); ok { - return validResponse.VisitStopRegisteringPresentationResponse(ctx.Response()) + } else if validResponse, ok := response.(UnregisterDIDResponseObject); ok { + return validResponse.VisitUnregisterDIDResponse(ctx.Response()) } else if response != nil { return fmt.Errorf("unexpected response type: %T", response) } return nil } -// StartRegisteringPresentation operation middleware -func (sh *strictHandler) StartRegisteringPresentation(ctx echo.Context, serviceID string, did string) error { - var request StartRegisteringPresentationRequestObject +// RegisterDID operation middleware +func (sh *strictHandler) RegisterDID(ctx echo.Context, serviceID string, did string) error { + var request RegisterDIDRequestObject request.ServiceID = serviceID request.Did = did handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.StartRegisteringPresentation(ctx.Request().Context(), request.(StartRegisteringPresentationRequestObject)) + return sh.ssi.RegisterDID(ctx.Request().Context(), request.(RegisterDIDRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "StartRegisteringPresentation") + handler = middleware(handler, "RegisterDID") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(StartRegisteringPresentationResponseObject); ok { - return validResponse.VisitStartRegisteringPresentationResponse(ctx.Response()) + } else if validResponse, ok := response.(RegisterDIDResponseObject); ok { + return validResponse.VisitRegisterDIDResponse(ctx.Response()) } else if response != nil { return fmt.Errorf("unexpected response type: %T", response) } diff --git a/discovery/api/v1/wrapper.go b/discovery/api/v1/wrapper.go index 33dee68d03..355d04eba4 100644 --- a/discovery/api/v1/wrapper.go +++ b/discovery/api/v1/wrapper.go @@ -105,15 +105,15 @@ func (w *Wrapper) SearchPresentations(_ context.Context, request SearchPresentat return SearchPresentations200JSONResponse(result), nil } -func (w *Wrapper) StartRegisteringPresentation(ctx context.Context, request StartRegisteringPresentationRequestObject) (StartRegisteringPresentationResponseObject, error) { +func (w *Wrapper) RegisterDID(ctx context.Context, request RegisterDIDRequestObject) (RegisterDIDResponseObject, error) { subjectDID, err := did.ParseDID(request.Did) if err != nil { return nil, err } - err = w.Client.StartRegistration(ctx, request.ServiceID, *subjectDID) + err = w.Client.Register(ctx, request.ServiceID, *subjectDID) if errors.Is(err, discovery.ErrRegistrationFailed) { // registration failed, but will be retried - return StartRegisteringPresentation202JSONResponse{ + return RegisterDID202JSONResponse{ Reason: err.Error(), }, nil } @@ -121,17 +121,17 @@ func (w *Wrapper) StartRegisteringPresentation(ctx context.Context, request Star // other error return nil, err } - return StartRegisteringPresentation200Response{}, nil + return RegisterDID200Response{}, nil } -func (w *Wrapper) StopRegisteringPresentation(ctx context.Context, request StopRegisteringPresentationRequestObject) (StopRegisteringPresentationResponseObject, error) { +func (w *Wrapper) UnregisterDID(ctx context.Context, request UnregisterDIDRequestObject) (UnregisterDIDResponseObject, error) { subjectDID, err := did.ParseDID(request.Did) if err != nil { return nil, err } - err = w.Client.StopRegistration(ctx, request.ServiceID, *subjectDID) + err = w.Client.Unregister(ctx, request.ServiceID, *subjectDID) if err != nil { return nil, err } - return StopRegisteringPresentation200Response{}, nil -} \ No newline at end of file + return UnregisterDID200Response{}, nil +} diff --git a/discovery/api/v1/wrapper_test.go b/discovery/api/v1/wrapper_test.go index 6204ec6bed..883d9cd12f 100644 --- a/discovery/api/v1/wrapper_test.go +++ b/discovery/api/v1/wrapper_test.go @@ -105,39 +105,39 @@ func TestWrapper_RegisterPresentation(t *testing.T) { }) } -func TestWrapper_StartRegisteringPresentation(t *testing.T) { +func TestWrapper_RegisterDID(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" test.client.EXPECT().StartRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) - response, err := test.wrapper.StartRegisteringPresentation(nil, StartRegisteringPresentationRequestObject{ + response, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) assert.NoError(t, err) - assert.IsType(t, StartRegisteringPresentation200Response{}, response) + assert.IsType(t, RegisterDID200Response{}, response) }) t.Run("ok, but registration failed", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" test.client.EXPECT().StartRegistration(gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrRegistrationFailed) - response, err := test.wrapper.StartRegisteringPresentation(nil, StartRegisteringPresentationRequestObject{ + response, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) assert.NoError(t, err) - assert.IsType(t, StartRegisteringPresentation202JSONResponse{}, response) + assert.IsType(t, RegisterDID202JSONResponse{}, response) }) t.Run("other error", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" test.client.EXPECT().StartRegistration(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo")) - _, err := test.wrapper.StartRegisteringPresentation(nil, StartRegisteringPresentationRequestObject{ + _, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) @@ -146,26 +146,26 @@ func TestWrapper_StartRegisteringPresentation(t *testing.T) { }) } -func TestWrapper_StopRegisteringPresentation(t *testing.T) { +func TestWrapper_UnregisterDID(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) - response, err := test.wrapper.StopRegisteringPresentation(nil, StopRegisteringPresentationRequestObject{ + response, err := test.wrapper.UnregisterDID(nil, UnregisterDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) assert.NoError(t, err) - assert.IsType(t, StopRegisteringPresentation200Response{}, response) + assert.IsType(t, UnregisterDID200Response{}, response) }) t.Run("error", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(errors.New("foo")) - _, err := test.wrapper.StopRegisteringPresentation(nil, StopRegisteringPresentationRequestObject{ + _, err := test.wrapper.UnregisterDID(nil, UnregisterDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) diff --git a/discovery/client.go b/discovery/client.go index de2d708d38..c5e59e37a8 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -35,14 +35,14 @@ import ( ) // registrationManager is responsible for managing registrations on a Discovery Service. -// It automatically refreshes registrations when they are about to expire. +// It automatically refreshes registered Verifiable Presentations when they are about to expire. type registrationManager interface { register(ctx context.Context, serviceID string, subjectDID did.DID) error unregister(ctx context.Context, serviceID string, subjectDID did.DID) error // refreshRegistrations is a blocking call to periodically refresh registrations. // It checks for registrations to be refreshed at the specified interval. // It will exit when the given context is cancelled. - refreshRegistrations(ctx context.Context, interval time.Duration) + refreshVerifiablePresentations(ctx context.Context, interval time.Duration) } var _ registrationManager = &scheduledRegistrationManager{} @@ -70,9 +70,9 @@ func (r *scheduledRegistrationManager) register(ctx context.Context, serviceID s return ErrServiceNotFound } // TODO: When to refresh? For now, we refresh when the registration is about to expire (75% of max age) - registrationRenewal := time.Now().Add(time.Duration(float64(service.PresentationMaxValidity)*0.75) * time.Second) - log.Logger().Debugf("Refreshing registration DID on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) - if err := r.store.updateDIDRegistrationTime(serviceID, subjectDID, ®istrationRenewal); err != nil { + refreshVPAfter := time.Now().Add(time.Duration(float64(service.PresentationMaxValidity)*0.75) * time.Second) + log.Logger().Debugf("Refreshing Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) + if err := r.store.updateDIDRegistrationTime(serviceID, subjectDID, &refreshVPAfter); err != nil { return fmt.Errorf("unable to update DID registration: %w", err) } err := r.registerPresentation(ctx, subjectDID, service) @@ -82,7 +82,7 @@ func (r *scheduledRegistrationManager) register(ctx context.Context, serviceID s _ = r.store.updateDIDRegistrationTime(serviceID, subjectDID, &next) return errors.Join(ErrRegistrationFailed, err) } - log.Logger().Debugf("Successfully refreshed registration DID on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) + log.Logger().Debugf("Successfully refreshed Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) return nil } @@ -159,27 +159,27 @@ func (r *scheduledRegistrationManager) buildPresentation(ctx context.Context, su }, &subjectDID, false) } -func (r *scheduledRegistrationManager) doRefreshRegistrations(ctx context.Context, now time.Time) error { - log.Logger().Debug("Renewing DID registrations on Discovery Services") +func (r *scheduledRegistrationManager) doRefreshVerifiablePresentations(ctx context.Context, now time.Time) error { + log.Logger().Debug("Refreshing Verifiable Presentations on Discovery Services") serviceIDs, dids, err := r.store.getStaleDIDRegistrations(now) if err != nil { return err } for i, serviceID := range serviceIDs { if err := r.register(ctx, serviceID, dids[i]); err != nil { - log.Logger().WithError(err).Warnf("Failed to renew DID registration (service=%s, did=%s)", serviceID, dids[i]) + log.Logger().WithError(err).Warnf("Failed to refresh Verifiable Presentation (service=%s, did=%s)", serviceID, dids[i]) } } return nil } -func (r *scheduledRegistrationManager) refreshRegistrations(ctx context.Context, interval time.Duration) { +func (r *scheduledRegistrationManager) refreshVerifiablePresentations(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() // do the first refresh immediately do := func() { - if err := r.doRefreshRegistrations(audit.Context(ctx, "app", ModuleName, "RefreshRegistration"), time.Now()); err != nil { - log.Logger().WithError(err).Errorf("Failed to renew DID registrations") + if err := r.doRefreshVerifiablePresentations(audit.Context(ctx, "app", ModuleName, "RefreshVerifiablePresentations"), time.Now()); err != nil { + log.Logger().WithError(err).Errorf("Failed to refresh Verifiable Presentations") } } do() diff --git a/discovery/client_test.go b/discovery/client_test.go index 2b9df22ba3..ab2755439e 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -145,11 +145,11 @@ func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.doRefreshRegistrations(audit.TestContext(), time.Now()) + err := manager.doRefreshVerifiablePresentations(audit.TestContext(), time.Now()) require.NoError(t, err) }) - t.Run("2 registrations to renew, first one fails, second one succeeds", func(t *testing.T) { + 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) @@ -170,7 +170,7 @@ func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { 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) - err := manager.doRefreshRegistrations(audit.TestContext(), time.Now()) + err := manager.doRefreshVerifiablePresentations(audit.TestContext(), time.Now()) require.NoError(t, err) }) @@ -194,7 +194,7 @@ func Test_scheduledRegistrationManager_refreshRegistrations(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - manager.refreshRegistrations(ctx, time.Millisecond) + manager.refreshVerifiablePresentations(ctx, time.Millisecond) }() // make sure the loop has at least once time.Sleep(5 * time.Millisecond) diff --git a/discovery/interface.go b/discovery/interface.go index c5d263c3bd..894fdae2c4 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -103,17 +103,16 @@ type Client interface { // Query parameters are formatted as simple JSON paths, e.g. "issuer" or "credentialSubject.name". Search(serviceID string, query map[string]string) ([]SearchResult, error) - // StartRegistration starts registration of presentations on a Discovery Service for the specified DID. + // Register causes a DID, in the form of a Verifiable Presentation, to be registered on a Discovery Service. // Registration will be attempted immediately, and automatically refreshed. // If initial registration failed, it will return ErrRegistrationFailed, but it will keep retrying. // If the function is called again for the same service/DID combination, it will try to refresh the registration. // It returns an error if the service or DID is invalid/unknown. - StartRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error + Register(ctx context.Context, serviceID string, subjectDID did.DID) error - // StopRegistration stops (automatic) registration of presentations on a Discovery Service for the specified DID. - // It will also try to delete the existing registration on the Discovery Service, if any. + // Unregister removes the registration of a DID on a Discovery Service. // It returns an error if the service or DID is invalid/unknown. - StopRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error + Unregister(ctx context.Context, serviceID string, subjectDID did.DID) error } // SearchResult is a single result of a search operation. diff --git a/discovery/mock.go b/discovery/mock.go index 016ab311aa..2c0714c323 100644 --- a/discovery/mock.go +++ b/discovery/mock.go @@ -110,9 +110,9 @@ func (mr *MockClientMockRecorder) Search(serviceID, query any) *gomock.Call { } // StartRegistration mocks base method. -func (m *MockClient) StartRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (m *MockClient) Register(ctx context.Context, serviceID string, subjectDID did.DID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StartRegistration", ctx, serviceID, subjectDID) + ret := m.ctrl.Call(m, "Register", ctx, serviceID, subjectDID) ret0, _ := ret[0].(error) return ret0 } @@ -120,13 +120,13 @@ func (m *MockClient) StartRegistration(ctx context.Context, serviceID string, su // StartRegistration indicates an expected call of StartRegistration. func (mr *MockClientMockRecorder) StartRegistration(ctx, serviceID, subjectDID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartRegistration", reflect.TypeOf((*MockClient)(nil).StartRegistration), ctx, serviceID, subjectDID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), ctx, serviceID, subjectDID) } // StopRegistration mocks base method. -func (m *MockClient) StopRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (m *MockClient) Unregister(ctx context.Context, serviceID string, subjectDID did.DID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StopRegistration", ctx, serviceID, subjectDID) + ret := m.ctrl.Call(m, "Unregister", ctx, serviceID, subjectDID) ret0, _ := ret[0].(error) return ret0 } @@ -134,5 +134,5 @@ func (m *MockClient) StopRegistration(ctx context.Context, serviceID string, sub // StopRegistration indicates an expected call of StopRegistration. func (mr *MockClientMockRecorder) StopRegistration(ctx, serviceID, subjectDID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopRegistration", reflect.TypeOf((*MockClient)(nil).StopRegistration), ctx, serviceID, subjectDID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unregister", reflect.TypeOf((*MockClient)(nil).Unregister), ctx, serviceID, subjectDID) } diff --git a/discovery/module.go b/discovery/module.go index 0f0fc4f16b..c256da8701 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -127,7 +127,7 @@ func (m *Module) Start() error { m.routines.Add(1) go func() { defer m.routines.Done() - m.registrationManager.refreshRegistrations(m.ctx, m.config.Client.RegistrationRefreshInterval) + m.registrationManager.refreshVerifiablePresentations(m.ctx, m.config.Client.RegistrationRefreshInterval) }() return nil } @@ -254,7 +254,7 @@ func (m *Module) Get(serviceID string, tag *Tag) ([]vc.VerifiablePresentation, * return m.store.get(serviceID, tag) } -func (m *Module) StartRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (m *Module) Register(ctx context.Context, serviceID string, subjectDID did.DID) error { log.Logger().Debugf("Registering on Discovery Service (did=%s, service=%s)", subjectDID, serviceID) err := m.registrationManager.register(ctx, serviceID, subjectDID) if errors.Is(err, ErrRegistrationFailed) { @@ -265,7 +265,7 @@ func (m *Module) StartRegistration(ctx context.Context, serviceID string, subjec return err } -func (m *Module) StopRegistration(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (m *Module) Unregister(ctx context.Context, serviceID string, subjectDID did.DID) error { log.Logger().Infof("Unregistering from Discovery Service (did=%s, service=%s)", subjectDID, serviceID) return m.registrationManager.unregister(ctx, serviceID, subjectDID) } diff --git a/discovery/store.go b/discovery/store.go index c1aa4581ae..c0d5e0cdb6 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -431,7 +431,7 @@ func (s *sqlStore) updateDIDRegistrationTime(serviceID string, subjectDID did.DI }) } -// getStaleDIDRegistrations returns all DID discovery service registrations that are due for renewal. +// getStaleDIDRegistrations returns all DID discovery service registrations that are due for refreshing. // It returns a slice of service IDs and associated DIDs. func (s *sqlStore) getStaleDIDRegistrations(now time.Time) ([]string, []did.DID, error) { var rows []didRegistrationRecord diff --git a/docs/_static/discovery/v1.yaml b/docs/_static/discovery/v1.yaml index e536b95305..0481efb955 100644 --- a/docs/_static/discovery/v1.yaml +++ b/docs/_static/discovery/v1.yaml @@ -1,7 +1,7 @@ openapi: "3.0.0" info: title: Nuts Discovery Service API spec - description: API specification for discovery services available within Nuts node + description: API specification for Discovery Services available within Nuts node version: 1.0.0 license: name: GPLv3 @@ -16,9 +16,9 @@ paths: schema: type: string get: - summary: Retrieves the presentations of a discovery service. + summary: Retrieves the presentations of a Discovery Service. description: | - An API provided by the discovery server to retrieve the presentations of a discovery service, starting at the given tag. + An API provided by the discovery server to retrieve the presentations of a Discovery Service, starting at the given tag. The client should provide the tag it was returned in the last response. If no tag is given, it will return all presentations. @@ -42,7 +42,7 @@ paths: default: $ref: "../common/error_response.yaml" post: - summary: Register a presentation on the discovery service. + summary: Register a presentation on the Discovery Service. description: | An API provided by the discovery server that adds a presentation to the service. The presentation must be signed by subject of the credentials it contains. @@ -58,7 +58,7 @@ paths: tags: - discovery requestBody: - description: The presentation to register to the discovery service. + description: The presentation to register to the Discovery Service. required: true content: application/json: @@ -66,7 +66,7 @@ paths: $ref: "#/components/schemas/VerifiablePresentation" responses: "201": - description: Presentation was registered on the discovery service. + description: Presentation was registered on the Discovery Service. "400": $ref: "../common/error_response.yaml" default: @@ -90,9 +90,9 @@ paths: style: form explode: true get: - summary: Searches for presentations registered on the discovery service. + summary: Searches for presentations registered on the Discovery Service. description: | - An API of the discovery client that searches for presentations on the discovery service, + An API of the discovery client that searches for presentations on the Discovery Service, whose credentials match the given query parameter. It queries the client's local copy of the Discovery Service which is periodically synchronized with the Discovery Server. This means new registrations might not immediately show up, depending on the client refresh interval. @@ -140,26 +140,26 @@ paths: schema: type: string post: - summary: Client API to start registering the given DID on the discovery service. + summary: Client API to register the given DID on the Discovery Service. description: | - An API provided by the discovery client that will cause the given DID to be registered on the specified discovery service. - Registration will be attempted immediately, and it will be automatically refreshed. + An API provided by the discovery client that will cause the given DID to be registered on the specified Discovery Service. + Registration of a Verifiable Presentation will be attempted immediately, and it will be automatically refreshed. Application only need to call this API once for every service/DID combination, until the registration is explicitly deleted through this API. - For successful registration on the discovery service, the DID's credential wallet must contain the credentials specified by the discovery service definition. + For successful registration on the Discovery Server, the DID's credential wallet must contain the credentials specified by the Discovery Service definition. If initial registration fails this API returns the error indicating what failed, but will retry at a later moment. Applications can force a retry by calling this API again. error returns: * 400 - incorrect input: invalid/unknown service or DID. - operationId: startRegisteringPresentation + operationId: registerDID tags: - discovery responses: "200": - description: Registration at discovery service was successful. + description: Registration at Discovery Service was successful. "202": - description: Registration at discovery service failed, but will be re-attempted later. + description: Registration at Discovery Service failed, but will be re-attempted later. content: application/json: schema: @@ -175,24 +175,24 @@ paths: default: $ref: "../common/error_response.yaml" delete: - summary: Client API to stop registering the given DID on the discovery service. + summary: Client API to unregister the given DID from the Discovery Service. description: | - An API provided by the discovery client that will cause the given DID to be not to be registered any more on the specified discovery service. - It will try to delete the existing registration at the discovery service, if any. + An API provided by the discovery client that will cause the given DID to be not to be registered any more on the specified Discovery Service. + It will try to delete the existing registration at the Discovery Service, if any. error returns: * 400 - incorrect input: invalid/unknown service or DID. - operationId: stopRegisteringPresentation + operationId: unregisterDID tags: - discovery responses: "200": description: | - Registration at discovery service was successfully stopped, and registration at discovery service was deleted - (if applicable). + DID was successfully unregistered from the Discovery Service. + Active Verifiable Presentation was removed from the remote Discovery Server (if applicable). "202": description: | - Registration at discovery service was successfully stopped, but failed to delete registration at discovery service. + DID was successfully unregistered from the Discovery Service, but failed to remove the active Verifiable Presentation registration from the remote Discovery Server. Applications might want to retry this API call later, or simply let the presentation expire. content: application/json: @@ -203,7 +203,7 @@ paths: properties: reason: type: string - description: Description of why registration deletion failed. + description: Description of why removal of the registration failed. "400": $ref: "../common/error_response.yaml" default: From 542aa749ea34236985ddc531ab657ea095d81dcf Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 9 Jan 2024 13:46:31 +0100 Subject: [PATCH 04/15] pr feedback --- discovery/api/v1/generated.go | 84 ++++++++++++++++---------------- discovery/api/v1/wrapper.go | 6 +-- discovery/api/v1/wrapper_test.go | 8 +-- discovery/client.go | 4 +- discovery/client_test.go | 10 ++-- discovery/interface.go | 4 +- discovery/mock.go | 40 +++++++-------- discovery/module.go | 6 +-- policy/mock.go | 35 ++++++------- 9 files changed, 99 insertions(+), 98 deletions(-) diff --git a/discovery/api/v1/generated.go b/discovery/api/v1/generated.go index 43572dfe1e..6c6d8dc70f 100644 --- a/discovery/api/v1/generated.go +++ b/discovery/api/v1/generated.go @@ -131,8 +131,8 @@ type ClientInterface interface { // SearchPresentations request SearchPresentations(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - // UnregisterDID request - UnregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeregisterDID request + DeregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) // RegisterDID request RegisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -186,8 +186,8 @@ func (c *Client) SearchPresentations(ctx context.Context, serviceID string, para return c.Client.Do(req) } -func (c *Client) UnregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewUnregisterDIDRequest(c.Server, serviceID, did) +func (c *Client) DeregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeregisterDIDRequest(c.Server, serviceID, did) if err != nil { return nil, err } @@ -365,8 +365,8 @@ func NewSearchPresentationsRequest(server string, serviceID string, params *Sear return req, nil } -// NewUnregisterDIDRequest generates requests for UnregisterDID -func NewUnregisterDIDRequest(server string, serviceID string, did string) (*http.Request, error) { +// NewDeregisterDIDRequest generates requests for DeregisterDID +func NewDeregisterDIDRequest(server string, serviceID string, did string) (*http.Request, error) { var err error var pathParam0 string @@ -501,8 +501,8 @@ type ClientWithResponsesInterface interface { // SearchPresentationsWithResponse request SearchPresentationsWithResponse(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*SearchPresentationsResponse, error) - // UnregisterDIDWithResponse request - UnregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*UnregisterDIDResponse, error) + // DeregisterDIDWithResponse request + DeregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*DeregisterDIDResponse, error) // RegisterDIDWithResponse request RegisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*RegisterDIDResponse, error) @@ -613,7 +613,7 @@ func (r SearchPresentationsResponse) StatusCode() int { return 0 } -type UnregisterDIDResponse struct { +type DeregisterDIDResponse struct { Body []byte HTTPResponse *http.Response JSON202 *struct { @@ -643,7 +643,7 @@ type UnregisterDIDResponse struct { } // Status returns HTTPResponse.Status -func (r UnregisterDIDResponse) Status() string { +func (r DeregisterDIDResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -651,7 +651,7 @@ func (r UnregisterDIDResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r UnregisterDIDResponse) StatusCode() int { +func (r DeregisterDIDResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -738,13 +738,13 @@ func (c *ClientWithResponses) SearchPresentationsWithResponse(ctx context.Contex return ParseSearchPresentationsResponse(rsp) } -// UnregisterDIDWithResponse request returning *UnregisterDIDResponse -func (c *ClientWithResponses) UnregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*UnregisterDIDResponse, error) { - rsp, err := c.UnregisterDID(ctx, serviceID, did, reqEditors...) +// DeregisterDIDWithResponse request returning *DeregisterDIDResponse +func (c *ClientWithResponses) DeregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*DeregisterDIDResponse, error) { + rsp, err := c.DeregisterDID(ctx, serviceID, did, reqEditors...) if err != nil { return nil, err } - return ParseUnregisterDIDResponse(rsp) + return ParseDeregisterDIDResponse(rsp) } // RegisterDIDWithResponse request returning *RegisterDIDResponse @@ -891,15 +891,15 @@ func ParseSearchPresentationsResponse(rsp *http.Response) (*SearchPresentationsR return response, nil } -// ParseUnregisterDIDResponse parses an HTTP response from a UnregisterDIDWithResponse call -func ParseUnregisterDIDResponse(rsp *http.Response) (*UnregisterDIDResponse, error) { +// ParseDeregisterDIDResponse parses an HTTP response from a DeregisterDIDWithResponse call +func ParseDeregisterDIDResponse(rsp *http.Response) (*DeregisterDIDResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UnregisterDIDResponse{ + response := &DeregisterDIDResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -1026,7 +1026,7 @@ type ServerInterface interface { SearchPresentations(ctx echo.Context, serviceID string, params SearchPresentationsParams) error // Client API to stop registering the given DID on the discovery service. // (DELETE /internal/discovery/v1/{serviceID}/{did}) - UnregisterDID(ctx echo.Context, serviceID string, did string) error + DeregisterDID(ctx echo.Context, serviceID string, did string) error // Client API to start registering the given DID on the discovery service. // (POST /internal/discovery/v1/{serviceID}/{did}) RegisterDID(ctx echo.Context, serviceID string, did string) error @@ -1109,8 +1109,8 @@ func (w *ServerInterfaceWrapper) SearchPresentations(ctx echo.Context) error { return err } -// UnregisterDID converts echo context to params. -func (w *ServerInterfaceWrapper) UnregisterDID(ctx echo.Context) error { +// DeregisterDID converts echo context to params. +func (w *ServerInterfaceWrapper) DeregisterDID(ctx echo.Context) error { var err error // ------------- Path parameter "serviceID" ------------- var serviceID string @@ -1131,7 +1131,7 @@ func (w *ServerInterfaceWrapper) UnregisterDID(ctx echo.Context) error { ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.UnregisterDID(ctx, serviceID, did) + err = w.Handler.DeregisterDID(ctx, serviceID, did) return err } @@ -1192,7 +1192,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/discovery/:serviceID", wrapper.GetPresentations) router.POST(baseURL+"/discovery/:serviceID", wrapper.RegisterPresentation) router.GET(baseURL+"/discovery/:serviceID/search", wrapper.SearchPresentations) - router.DELETE(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.UnregisterDID) + router.DELETE(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.DeregisterDID) router.POST(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.RegisterDID) } @@ -1331,36 +1331,36 @@ func (response SearchPresentationsdefaultApplicationProblemPlusJSONResponse) Vis return json.NewEncoder(w).Encode(response.Body) } -type UnregisterDIDRequestObject struct { +type DeregisterDIDRequestObject struct { ServiceID string `json:"serviceID"` Did string `json:"did"` } -type UnregisterDIDResponseObject interface { - VisitUnregisterDIDResponse(w http.ResponseWriter) error +type DeregisterDIDResponseObject interface { + VisitDeregisterDIDResponse(w http.ResponseWriter) error } -type UnregisterDID200Response struct { +type DeregisterDID200Response struct { } -func (response UnregisterDID200Response) VisitUnregisterDIDResponse(w http.ResponseWriter) error { +func (response DeregisterDID200Response) VisitDeregisterDIDResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type UnregisterDID202JSONResponse struct { +type DeregisterDID202JSONResponse struct { // Reason Description of why registration deletion failed. Reason string `json:"reason"` } -func (response UnregisterDID202JSONResponse) VisitUnregisterDIDResponse(w http.ResponseWriter) error { +func (response DeregisterDID202JSONResponse) VisitDeregisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(202) return json.NewEncoder(w).Encode(response) } -type UnregisterDID400ApplicationProblemPlusJSONResponse struct { +type DeregisterDID400ApplicationProblemPlusJSONResponse struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1371,14 +1371,14 @@ type UnregisterDID400ApplicationProblemPlusJSONResponse struct { Title string `json:"title"` } -func (response UnregisterDID400ApplicationProblemPlusJSONResponse) VisitUnregisterDIDResponse(w http.ResponseWriter) error { +func (response DeregisterDID400ApplicationProblemPlusJSONResponse) VisitDeregisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type UnregisterDIDdefaultApplicationProblemPlusJSONResponse struct { +type DeregisterDIDdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1392,7 +1392,7 @@ type UnregisterDIDdefaultApplicationProblemPlusJSONResponse struct { StatusCode int } -func (response UnregisterDIDdefaultApplicationProblemPlusJSONResponse) VisitUnregisterDIDResponse(w http.ResponseWriter) error { +func (response DeregisterDIDdefaultApplicationProblemPlusJSONResponse) VisitDeregisterDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) @@ -1480,7 +1480,7 @@ type StrictServerInterface interface { SearchPresentations(ctx context.Context, request SearchPresentationsRequestObject) (SearchPresentationsResponseObject, error) // Client API to stop registering the given DID on the discovery service. // (DELETE /internal/discovery/v1/{serviceID}/{did}) - UnregisterDID(ctx context.Context, request UnregisterDIDRequestObject) (UnregisterDIDResponseObject, error) + DeregisterDID(ctx context.Context, request DeregisterDIDRequestObject) (DeregisterDIDResponseObject, error) // Client API to start registering the given DID on the discovery service. // (POST /internal/discovery/v1/{serviceID}/{did}) RegisterDID(ctx context.Context, request RegisterDIDRequestObject) (RegisterDIDResponseObject, error) @@ -1581,26 +1581,26 @@ func (sh *strictHandler) SearchPresentations(ctx echo.Context, serviceID string, return nil } -// UnregisterDID operation middleware -func (sh *strictHandler) UnregisterDID(ctx echo.Context, serviceID string, did string) error { - var request UnregisterDIDRequestObject +// DeregisterDID operation middleware +func (sh *strictHandler) DeregisterDID(ctx echo.Context, serviceID string, did string) error { + var request DeregisterDIDRequestObject request.ServiceID = serviceID request.Did = did handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.UnregisterDID(ctx.Request().Context(), request.(UnregisterDIDRequestObject)) + return sh.ssi.DeregisterDID(ctx.Request().Context(), request.(DeregisterDIDRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "UnregisterDID") + handler = middleware(handler, "DeregisterDID") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(UnregisterDIDResponseObject); ok { - return validResponse.VisitUnregisterDIDResponse(ctx.Response()) + } else if validResponse, ok := response.(DeregisterDIDResponseObject); ok { + return validResponse.VisitDeregisterDIDResponse(ctx.Response()) } else if response != nil { return fmt.Errorf("unexpected response type: %T", response) } diff --git a/discovery/api/v1/wrapper.go b/discovery/api/v1/wrapper.go index 355d04eba4..9c8f1212f4 100644 --- a/discovery/api/v1/wrapper.go +++ b/discovery/api/v1/wrapper.go @@ -124,14 +124,14 @@ func (w *Wrapper) RegisterDID(ctx context.Context, request RegisterDIDRequestObj return RegisterDID200Response{}, nil } -func (w *Wrapper) UnregisterDID(ctx context.Context, request UnregisterDIDRequestObject) (UnregisterDIDResponseObject, error) { +func (w *Wrapper) DeregisterDID(ctx context.Context, request DeregisterDIDRequestObject) (DeregisterDIDResponseObject, error) { subjectDID, err := did.ParseDID(request.Did) if err != nil { return nil, err } - err = w.Client.Unregister(ctx, request.ServiceID, *subjectDID) + err = w.Client.Deregister(ctx, request.ServiceID, *subjectDID) if err != nil { return nil, err } - return UnregisterDID200Response{}, nil + return DeregisterDID200Response{}, nil } diff --git a/discovery/api/v1/wrapper_test.go b/discovery/api/v1/wrapper_test.go index 883d9cd12f..fa6262948b 100644 --- a/discovery/api/v1/wrapper_test.go +++ b/discovery/api/v1/wrapper_test.go @@ -146,26 +146,26 @@ func TestWrapper_RegisterDID(t *testing.T) { }) } -func TestWrapper_UnregisterDID(t *testing.T) { +func TestWrapper_DeregisterDID(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) - response, err := test.wrapper.UnregisterDID(nil, UnregisterDIDRequestObject{ + response, err := test.wrapper.DeregisterDID(nil, DeregisterDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) assert.NoError(t, err) - assert.IsType(t, UnregisterDID200Response{}, response) + assert.IsType(t, DeregisterDID200Response{}, response) }) t.Run("error", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(errors.New("foo")) - _, err := test.wrapper.UnregisterDID(nil, UnregisterDIDRequestObject{ + _, err := test.wrapper.DeregisterDID(nil, DeregisterDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) diff --git a/discovery/client.go b/discovery/client.go index c5e59e37a8..0352ac2509 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -38,7 +38,7 @@ import ( // It automatically refreshes registered Verifiable Presentations when they are about to expire. type registrationManager interface { register(ctx context.Context, serviceID string, subjectDID did.DID) error - unregister(ctx context.Context, serviceID string, subjectDID did.DID) error + deregister(ctx context.Context, serviceID string, subjectDID did.DID) error // refreshRegistrations is a blocking call to periodically refresh registrations. // It checks for registrations to be refreshed at the specified interval. // It will exit when the given context is cancelled. @@ -86,7 +86,7 @@ func (r *scheduledRegistrationManager) register(ctx context.Context, serviceID s return nil } -func (r *scheduledRegistrationManager) unregister(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (r *scheduledRegistrationManager) deregister(ctx context.Context, serviceID string, subjectDID did.DID) error { // delete DID/service combination from DB, so it won't be registered again err := r.store.updateDIDRegistrationTime(serviceID, subjectDID, nil) if err != nil { diff --git a/discovery/client_test.go b/discovery/client_test.go index ab2755439e..b7882bbd63 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -82,7 +82,7 @@ func Test_scheduledRegistrationManager_register(t *testing.T) { }) } -func Test_scheduledRegistrationManager_unregister(t *testing.T) { +func Test_scheduledRegistrationManager_deregister(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) @@ -93,7 +93,7 @@ func Test_scheduledRegistrationManager_unregister(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.unregister(audit.TestContext(), testServiceID, aliceDID) + err := manager.deregister(audit.TestContext(), testServiceID, aliceDID) assert.NoError(t, err) }) @@ -110,11 +110,11 @@ func Test_scheduledRegistrationManager_unregister(t *testing.T) { tag := Tag("taggy") require.NoError(t, store.add(testServiceID, vpAlice, &tag)) - err := manager.unregister(audit.TestContext(), testServiceID, aliceDID) + err := manager.deregister(audit.TestContext(), testServiceID, aliceDID) assert.NoError(t, err) }) - t.Run("unregistering from Discovery Service fails", func(t *testing.T) { + 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")) @@ -127,7 +127,7 @@ func Test_scheduledRegistrationManager_unregister(t *testing.T) { tag := Tag("taggy") require.NoError(t, store.add(testServiceID, vpAlice, &tag)) - err := manager.unregister(audit.TestContext(), testServiceID, aliceDID) + err := manager.deregister(audit.TestContext(), testServiceID, aliceDID) require.ErrorIs(t, err, ErrRegistrationFailed) require.ErrorContains(t, err, "remote error") diff --git a/discovery/interface.go b/discovery/interface.go index 894fdae2c4..e81294c94f 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -110,9 +110,9 @@ type Client interface { // It returns an error if the service or DID is invalid/unknown. Register(ctx context.Context, serviceID string, subjectDID did.DID) error - // Unregister removes the registration of a DID on a Discovery Service. + // Deregister removes the registration of a DID on a Discovery Service. // It returns an error if the service or DID is invalid/unknown. - Unregister(ctx context.Context, serviceID string, subjectDID did.DID) error + Deregister(ctx context.Context, serviceID string, subjectDID did.DID) error } // SearchResult is a single result of a search operation. diff --git a/discovery/mock.go b/discovery/mock.go index 2c0714c323..fc88be0e78 100644 --- a/discovery/mock.go +++ b/discovery/mock.go @@ -94,22 +94,21 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } -// Search mocks base method. -func (m *MockClient) Search(serviceID string, query map[string]string) ([]SearchResult, error) { +// Deregister mocks base method. +func (m *MockClient) Deregister(ctx context.Context, serviceID string, subjectDID did.DID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Search", serviceID, query) - ret0, _ := ret[0].([]SearchResult) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "Deregister", ctx, serviceID, subjectDID) + ret0, _ := ret[0].(error) + return ret0 } -// Search indicates an expected call of Search. -func (mr *MockClientMockRecorder) Search(serviceID, query any) *gomock.Call { +// Deregister indicates an expected call of Deregister. +func (mr *MockClientMockRecorder) Deregister(ctx, serviceID, subjectDID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockClient)(nil).Search), serviceID, query) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deregister", reflect.TypeOf((*MockClient)(nil).Deregister), ctx, serviceID, subjectDID) } -// StartRegistration mocks base method. +// Register mocks base method. func (m *MockClient) Register(ctx context.Context, serviceID string, subjectDID did.DID) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Register", ctx, serviceID, subjectDID) @@ -117,22 +116,23 @@ func (m *MockClient) Register(ctx context.Context, serviceID string, subjectDID return ret0 } -// StartRegistration indicates an expected call of StartRegistration. -func (mr *MockClientMockRecorder) StartRegistration(ctx, serviceID, subjectDID any) *gomock.Call { +// Register indicates an expected call of Register. +func (mr *MockClientMockRecorder) Register(ctx, serviceID, subjectDID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), ctx, serviceID, subjectDID) } -// StopRegistration mocks base method. -func (m *MockClient) Unregister(ctx context.Context, serviceID string, subjectDID did.DID) error { +// Search mocks base method. +func (m *MockClient) Search(serviceID string, query map[string]string) ([]SearchResult, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Unregister", ctx, serviceID, subjectDID) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "Search", serviceID, query) + ret0, _ := ret[0].([]SearchResult) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// StopRegistration indicates an expected call of StopRegistration. -func (mr *MockClientMockRecorder) StopRegistration(ctx, serviceID, subjectDID any) *gomock.Call { +// Search indicates an expected call of Search. +func (mr *MockClientMockRecorder) Search(serviceID, query any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unregister", reflect.TypeOf((*MockClient)(nil).Unregister), ctx, serviceID, subjectDID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockClient)(nil).Search), serviceID, query) } diff --git a/discovery/module.go b/discovery/module.go index c256da8701..eb5b772e1d 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -265,9 +265,9 @@ func (m *Module) Register(ctx context.Context, serviceID string, subjectDID did. return err } -func (m *Module) Unregister(ctx context.Context, serviceID string, subjectDID did.DID) error { - log.Logger().Infof("Unregistering from Discovery Service (did=%s, service=%s)", subjectDID, serviceID) - return m.registrationManager.unregister(ctx, serviceID, subjectDID) +func (m *Module) Deregister(ctx context.Context, serviceID string, subjectDID did.DID) error { + log.Logger().Infof("Deregistering from Discovery Service (did=%s, service=%s)", subjectDID, serviceID) + return m.registrationManager.deregister(ctx, serviceID, subjectDID) } func loadDefinitions(directory string) (map[string]ServiceDefinition, error) { diff --git a/policy/mock.go b/policy/mock.go index cc32c7811f..26c7e4987e 100644 --- a/policy/mock.go +++ b/policy/mock.go @@ -5,6 +5,7 @@ // // mockgen -destination=policy/mock.go -package=policy -source=policy/interface.go // + // Package policy is a generated GoMock package. package policy @@ -18,31 +19,31 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockBackend is a mock of PDPBackend interface. -type MockBackend struct { +// MockPDPBackend is a mock of PDPBackend interface. +type MockPDPBackend struct { ctrl *gomock.Controller - recorder *MockBackendMockRecorder + recorder *MockPDPBackendMockRecorder } -// MockBackendMockRecorder is the mock recorder for MockBackend. -type MockBackendMockRecorder struct { - mock *MockBackend +// MockPDPBackendMockRecorder is the mock recorder for MockPDPBackend. +type MockPDPBackendMockRecorder struct { + mock *MockPDPBackend } -// NewMockBackend creates a new mock instance. -func NewMockBackend(ctrl *gomock.Controller) *MockBackend { - mock := &MockBackend{ctrl: ctrl} - mock.recorder = &MockBackendMockRecorder{mock} +// NewMockPDPBackend creates a new mock instance. +func NewMockPDPBackend(ctrl *gomock.Controller) *MockPDPBackend { + mock := &MockPDPBackend{ctrl: ctrl} + mock.recorder = &MockPDPBackendMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockBackend) EXPECT() *MockBackendMockRecorder { +func (m *MockPDPBackend) EXPECT() *MockPDPBackendMockRecorder { return m.recorder } // Authorized mocks base method. -func (m *MockBackend) Authorized(ctx context.Context, requestInfo client.AuthorizedRequest) (bool, error) { +func (m *MockPDPBackend) Authorized(ctx context.Context, requestInfo client.AuthorizedRequest) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Authorized", ctx, requestInfo) ret0, _ := ret[0].(bool) @@ -51,13 +52,13 @@ func (m *MockBackend) Authorized(ctx context.Context, requestInfo client.Authori } // Authorized indicates an expected call of Authorized. -func (mr *MockBackendMockRecorder) Authorized(ctx, requestInfo any) *gomock.Call { +func (mr *MockPDPBackendMockRecorder) Authorized(ctx, requestInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorized", reflect.TypeOf((*MockBackend)(nil).Authorized), ctx, requestInfo) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorized", reflect.TypeOf((*MockPDPBackend)(nil).Authorized), ctx, requestInfo) } // PresentationDefinition mocks base method. -func (m *MockBackend) PresentationDefinition(ctx context.Context, authorizer did.DID, scope string) (*pe.PresentationDefinition, error) { +func (m *MockPDPBackend) PresentationDefinition(ctx context.Context, authorizer did.DID, scope string) (*pe.PresentationDefinition, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PresentationDefinition", ctx, authorizer, scope) ret0, _ := ret[0].(*pe.PresentationDefinition) @@ -66,7 +67,7 @@ func (m *MockBackend) PresentationDefinition(ctx context.Context, authorizer did } // PresentationDefinition indicates an expected call of PresentationDefinition. -func (mr *MockBackendMockRecorder) PresentationDefinition(ctx, authorizer, scope any) *gomock.Call { +func (mr *MockPDPBackendMockRecorder) PresentationDefinition(ctx, authorizer, scope any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PresentationDefinition", reflect.TypeOf((*MockBackend)(nil).PresentationDefinition), ctx, authorizer, scope) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PresentationDefinition", reflect.TypeOf((*MockPDPBackend)(nil).PresentationDefinition), ctx, authorizer, scope) } From 6a90a857740a1a102c2395d32001a09cd3f2c5a7 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 10 Jan 2024 06:51:24 +0100 Subject: [PATCH 05/15] pr feedback --- README.rst | 4 ++-- discovery/cmd/cmd.go | 3 ++- docs/pages/deployment/cli-reference.rst | 2 +- docs/pages/deployment/server_options.rst | 4 ++-- .../003_discoveryservice_client_registration.sql | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index b7c8ca9f0b..a52f0031a1 100644 --- a/README.rst +++ b/README.rst @@ -208,7 +208,7 @@ The following options can be configured on the server: crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. **Discovery** - discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). + discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services,in Golang time.Duration string format (e.g. 1s). Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. **Events** @@ -228,7 +228,7 @@ The following options can be configured on the server: http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. **JSONLD** - jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. **Network** network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. diff --git a/discovery/cmd/cmd.go b/discovery/cmd/cmd.go index b613836a35..bbe1d90f80 100644 --- a/discovery/cmd/cmd.go +++ b/discovery/cmd/cmd.go @@ -34,7 +34,8 @@ func FlagSet() *pflag.FlagSet { "IDs of the Discovery Service Definitions for which to act as server. "+ "If an ID does not map to a loaded service definition, the node will fail to start.") flagSet.Duration("discovery.client.registration_refresh_interval", defs.Client.RegistrationRefreshInterval, - "Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. "+ + "Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services,"+ + "in Golang time.Duration string format (e.g. 1s). "+ "Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left).") return flagSet } diff --git a/docs/pages/deployment/cli-reference.rst b/docs/pages/deployment/cli-reference.rst index 3fac03da12..71e41b9d99 100755 --- a/docs/pages/deployment/cli-reference.rst +++ b/docs/pages/deployment/cli-reference.rst @@ -29,7 +29,7 @@ The following options apply to the server commands below: --crypto.vault.timeout duration Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). (default 5s) --crypto.vault.token string The Vault token. If set it overwrites the VAULT_TOKEN env var. --datadir string Directory where the node stores its files. (default "./data") - --discovery.client.registration_refresh_interval duration Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). (default 10m0s) + --discovery.client.registration_refresh_interval duration Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services,in Golang time.Duration string format (e.g. 1s). Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). (default 10m0s) --discovery.definitions.directory string 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.definition_ids strings IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. --events.nats.hostname string Hostname for the NATS server (default "0.0.0.0") diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 1729f6fc8f..508cff480b 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -34,7 +34,7 @@ crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. **Discovery** - discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services. Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). + discovery.client.registration_refresh_interval 10m0s Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services,in Golang time.Duration string format (e.g. 1s). Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left). discovery.definitions.directory 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.definition_ids [] IDs of the Discovery Service Definitions for which to act as server. If an ID does not map to a loaded service definition, the node will fail to start. **Events** @@ -54,7 +54,7 @@ http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. **JSONLD** - jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. **Network** network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. diff --git a/storage/sql_migrations/003_discoveryservice_client_registration.sql b/storage/sql_migrations/003_discoveryservice_client_registration.sql index 937c39d0b4..46a9baadf1 100644 --- a/storage/sql_migrations/003_discoveryservice_client_registration.sql +++ b/storage/sql_migrations/003_discoveryservice_client_registration.sql @@ -6,7 +6,7 @@ create table discovery_did_registration -- It comes from the service definition. service_id varchar(200) not null, -- did is the DID that should be registered on the Discovery Service. - did varchar not null, + did varchar(500) not null, -- next_registration is the timestamp (seconds since Unix epoch) when the registration on the -- Discovery Service should be refreshed. next_registration integer not null, From a1a3ddd1afd57fbbe38d052b92edce852bf9c120 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 10 Jan 2024 08:54:08 +0100 Subject: [PATCH 06/15] use IsOwned --- cmd/root.go | 2 +- discovery/module.go | 16 +- discovery/module_test.go | 80 +++-- docs/diagrams/deployment-diagram.drawio | 438 +++++++++++++++++++++++- 4 files changed, 506 insertions(+), 30 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 51ae167281..34d17ca548 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -196,7 +196,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) + discoveryInstance := discovery.New(storageInstance, credentialInstance, vdrInstance) authInstance := auth.NewAuthInstance(auth.DefaultConfig(), vdrInstance, credentialInstance, cryptoInstance, didmanInstance, jsonld, pkiInstance) statusEngine := status.NewStatusEngine(system) metricsEngine := core.NewMetricsEngine() diff --git a/discovery/module.go b/discovery/module.go index eb5b772e1d..ce1713634f 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -31,6 +31,7 @@ import ( "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" + "github.com/nuts-foundation/nuts-node/vdr/management" "os" "path" "strings" @@ -67,10 +68,11 @@ var _ Client = &Module{} var retractionPresentationType = ssi.MustParseURI("RetractedVerifiablePresentation") // New creates a new Module. -func New(storageInstance storage.Engine, vcrInstance vcr.VCR) *Module { +func New(storageInstance storage.Engine, vcrInstance vcr.VCR, documentOwner management.DocumentOwner) *Module { m := &Module{ storageInstance: storageInstance, vcrInstance: vcrInstance, + documentOwner: documentOwner, } m.ctx, m.cancel = context.WithCancel(context.Background()) m.routines = new(sync.WaitGroup) @@ -87,6 +89,7 @@ type Module struct { serverDefinitions map[string]ServiceDefinition allDefinitions map[string]ServiceDefinition vcrInstance vcr.VCR + documentOwner management.DocumentOwner ctx context.Context cancel context.CancelFunc routines *sync.WaitGroup @@ -256,10 +259,17 @@ func (m *Module) Get(serviceID string, tag *Tag) ([]vc.VerifiablePresentation, * func (m *Module) Register(ctx context.Context, serviceID string, subjectDID did.DID) error { log.Logger().Debugf("Registering on Discovery Service (did=%s, service=%s)", subjectDID, serviceID) - err := m.registrationManager.register(ctx, serviceID, subjectDID) + isOwner, err := m.documentOwner.IsOwner(ctx, subjectDID) + if err != nil { + return err + } + if !isOwner { + return errors.New("not owner of DID") + } + err = m.registrationManager.register(ctx, serviceID, subjectDID) if errors.Is(err, ErrRegistrationFailed) { log.Logger().WithError(err).Warnf("Discovery Service registration failed, will be retried later (did=%s,service=%s)", subjectDID, serviceID) - } else { + } else if err == nil { log.Logger().Infof("Successfully registered Discovery Service (did=%s,service=%s)", subjectDID, serviceID) } return err diff --git a/discovery/module_test.go b/discovery/module_test.go index e978dfbea0..d1d4201d4b 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -19,6 +19,7 @@ package discovery import ( + "context" "encoding/json" "errors" "github.com/lestrrat-go/jwx/v2/jwt" @@ -26,7 +27,9 @@ import ( "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" + "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vcr/verifier" + "github.com/nuts-foundation/nuts-node/vdr/management" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -39,7 +42,7 @@ func TestModule_Name(t *testing.T) { } func TestModule_Shutdown(t *testing.T) { - module, _ := setupModule(t, storage.NewTestStorageEngine(t)) + module, _, _ := setupModule(t, storage.NewTestStorageEngine(t)) require.NoError(t, module.Shutdown()) } @@ -48,13 +51,13 @@ func Test_Module_Add(t *testing.T) { require.NoError(t, storageEngine.Start()) t.Run("not a server", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) err := m.Add("other", vpAlice) require.EqualError(t, err, "node is not a discovery server for this service") }) t.Run("VP verification fails (e.g. invalid signature)", func(t *testing.T) { - m, presentationVerifier := setupModule(t, storageEngine) + m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("failed")) err := m.Add(testServiceID, vpAlice) @@ -66,7 +69,7 @@ func Test_Module_Add(t *testing.T) { assert.Equal(t, expectedTag, *tag) }) t.Run("already exists", func(t *testing.T) { - m, presentationVerifier := setupModule(t, storageEngine) + m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) err := m.Add(testServiceID, vpAlice) @@ -75,7 +78,7 @@ func Test_Module_Add(t *testing.T) { assert.ErrorIs(t, err, ErrPresentationAlreadyExists) }) t.Run("valid for too long", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) def := m.allDefinitions[testServiceID] def.PresentationMaxValidity = 1 m.allDefinitions[testServiceID] = def @@ -85,7 +88,7 @@ func Test_Module_Add(t *testing.T) { assert.EqualError(t, err, "presentation is invalid for registration\npresentation is valid for too long (max 1s)") }) t.Run("no expiration", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) err := m.Add(testServiceID, createPresentationCustom(aliceDID, func(claims map[string]interface{}, _ *vc.VerifiablePresentation) { claims[jwt.AudienceKey] = []string{testServiceID} delete(claims, "exp") @@ -93,7 +96,7 @@ func Test_Module_Add(t *testing.T) { assert.ErrorIs(t, err, errPresentationWithoutExpiration) }) t.Run("presentation does not contain an ID", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) vpWithoutID := createPresentationCustom(aliceDID, func(claims map[string]interface{}, _ *vc.VerifiablePresentation) { claims[jwt.AudienceKey] = []string{testServiceID} @@ -103,14 +106,14 @@ func Test_Module_Add(t *testing.T) { assert.ErrorIs(t, err, errPresentationWithoutID) }) t.Run("not a JWT", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) err := m.Add(testServiceID, vc.VerifiablePresentation{}) assert.ErrorIs(t, err, errUnsupportedPresentationFormat) }) t.Run("registration", func(t *testing.T) { t.Run("ok", func(t *testing.T) { - m, presentationVerifier := setupModule(t, storageEngine) + m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil) err := m.Add(testServiceID, vpAlice) @@ -121,7 +124,7 @@ func Test_Module_Add(t *testing.T) { assert.Equal(t, "1", string(*tag)[tagPrefixLength:]) }) t.Run("valid longer than its credentials", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) vcAlice := createCredential(authorityDID, aliceDID, nil, func(claims map[string]interface{}) { claims[jwt.AudienceKey] = []string{testServiceID} @@ -134,7 +137,7 @@ func Test_Module_Add(t *testing.T) { assert.ErrorIs(t, err, errPresentationValidityExceedsCredentials) }) t.Run("not conform to Presentation Definition", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) // Presentation Definition only allows did:example DIDs otherVP := createPresentationCustom(unsupportedDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { @@ -154,7 +157,7 @@ func Test_Module_Add(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} }) t.Run("ok", func(t *testing.T) { - m, presentationVerifier := setupModule(t, storageEngine) + m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).Times(2) err := m.Add(testServiceID, vpAlice) @@ -163,12 +166,12 @@ func Test_Module_Add(t *testing.T) { assert.NoError(t, err) }) t.Run("non-existent presentation", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) err := m.Add(testServiceID, vpAliceRetract) assert.ErrorIs(t, err, errRetractionReferencesUnknownPresentation) }) t.Run("must not contain credentials", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) vp := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { vp.Type = append(vp.Type, retractionPresentationType) claims[jwt.AudienceKey] = []string{testServiceID} @@ -177,7 +180,7 @@ func Test_Module_Add(t *testing.T) { assert.ErrorIs(t, err, errRetractionContainsCredentials) }) t.Run("missing 'retract_jti' claim", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) vp := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { vp.Type = append(vp.Type, retractionPresentationType) claims[jwt.AudienceKey] = []string{testServiceID} @@ -186,7 +189,7 @@ func Test_Module_Add(t *testing.T) { assert.ErrorIs(t, err, errInvalidRetractionJTIClaim) }) t.Run("'retract_jti' claim is not a string", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) vp := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { vp.Type = append(vp.Type, retractionPresentationType) claims["retract_jti"] = 10 @@ -196,7 +199,7 @@ func Test_Module_Add(t *testing.T) { assert.ErrorIs(t, err, errInvalidRetractionJTIClaim) }) t.Run("'retract_jti' claim is an empty string", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) vp := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { vp.Type = append(vp.Type, retractionPresentationType) claims["retract_jti"] = "" @@ -212,7 +215,7 @@ func Test_Module_Get(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) t.Run("ok", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) require.NoError(t, m.store.add(testServiceID, vpAlice, nil)) presentations, tag, err := m.Get(testServiceID, nil) assert.NoError(t, err) @@ -220,26 +223,27 @@ func Test_Module_Get(t *testing.T) { assert.Equal(t, "1", string(*tag)[tagPrefixLength:]) }) t.Run("ok - retrieve delta", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) require.NoError(t, m.store.add(testServiceID, vpAlice, nil)) presentations, _, err := m.Get(testServiceID, nil) require.NoError(t, err) require.Len(t, presentations, 1) }) t.Run("not a server for this service ID", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) _, _, err := m.Get("other", nil) assert.ErrorIs(t, err, ErrServerModeDisabled) }) } -func setupModule(t *testing.T, storageInstance storage.Engine) (*Module, *verifier.MockVerifier) { +func setupModule(t *testing.T, storageInstance storage.Engine) (*Module, *verifier.MockVerifier, *management.MockDocumentOwner) { resetStore(t, storageInstance.GetSQLDatabase()) ctrl := gomock.NewController(t) mockVerifier := verifier.NewMockVerifier(ctrl) mockVCR := vcr.NewMockVCR(ctrl) mockVCR.EXPECT().Verifier().Return(mockVerifier).AnyTimes() - m := New(storageInstance, mockVCR) + documentOwner := management.NewMockDocumentOwner(ctrl) + m := New(storageInstance, mockVCR, documentOwner) m.config = DefaultConfig() require.NoError(t, m.Configure(core.ServerConfig{})) m.allDefinitions = testDefinitions() @@ -247,7 +251,7 @@ func setupModule(t *testing.T, storageInstance storage.Engine) (*Module, *verifi testServiceID: m.allDefinitions[testServiceID], } require.NoError(t, m.Start()) - return m, mockVerifier + return m, mockVerifier, documentOwner } func TestModule_Configure(t *testing.T) { @@ -294,7 +298,7 @@ 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) + m, _, _ := setupModule(t, storageEngine) require.NoError(t, m.store.add(testServiceID, vpAlice, nil)) results, err := m.Search(testServiceID, map[string]string{ "credentialSubject.id": aliceDID.String(), @@ -310,8 +314,34 @@ func TestModule_Search(t *testing.T) { assert.JSONEq(t, string(expectedJSON), string(actualJSON)) }) t.Run("unknown service ID", func(t *testing.T) { - m, _ := setupModule(t, storageEngine) + m, _, _ := setupModule(t, storageEngine) _, err := m.Search("unknown", nil) assert.ErrorIs(t, err, ErrServiceNotFound) }) } + +func TestModule_Register(t *testing.T) { + t.Run("not owned", func(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + m, _, documentOwner := setupModule(t, storageEngine) + documentOwner.EXPECT().IsOwner(gomock.Any(), aliceDID).Return(false, nil) + + err := m.Register(context.Background(), testServiceID, aliceDID) + + require.EqualError(t, err, "not owner of DID") + }) + t.Run("ok, but couldn't register presentation -> maps to ErrRegistrationFailed", func(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + m, _, documentOwner := setupModule(t, storageEngine) + wallet := holder.NewMockWallet(gomock.NewController(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) + documentOwner.EXPECT().IsOwner(gomock.Any(), aliceDID).Return(true, nil) + + err := m.Register(context.Background(), testServiceID, aliceDID) + + require.ErrorIs(t, err, ErrRegistrationFailed) + }) +} diff --git a/docs/diagrams/deployment-diagram.drawio b/docs/diagrams/deployment-diagram.drawio index 927d656c69..84cdaff097 100644 --- a/docs/diagrams/deployment-diagram.drawio +++ b/docs/diagrams/deployment-diagram.drawio @@ -1 +1,437 @@ -7V1rd6I6F/41XWfOh7rCTeRjq22nM7Z1tLeZL7MQojJFYrlUO7/+TbgHAqKC2r5nzlqnGgIC2fvZ950ToTtfXdnqYnaDdGie8EBfnQi9E57nuE4b/yEj78FIh3wjA1Pb0MNJycDI+AvDQRCOeoYOHWqii5DpGgt6UEOWBTWXGlNtGy3paRNk0r+6UKcwNzDSVDM/+mTo7ix6CjkZ/wqN6Sz6Za6tBEfmajQ5fBJnpupomRoSLk6Ero2QG3yar7rQJC8vei/BeZcFR+Mbs6HlVjlhxf/w3FMT/Jp9X/3+MdQ17fLqNLzKm2p64QOf8G0TX+98gvBl8V277+GraL96KDpw6vgLdYYncNwCL/Z5chx/mpK/N6pF3i1eGtOA/rV61z0nuvzYjuadSPgU8PX+fhBcED+BYbnQtlRy9ydSLzoFHwhuKjwveKvx/fFQxysXfkW2O0NThK9xkYye28izdEjeB8Dfkjl9hBZ4kMODf6DrvodkqHouwkMzd26GR8nvhwc5cgn8XPb7c3g9/8tP8qUlRV97q/TB3nv4Lb924UI4yLM1WLJgIfe4qj2Fbsm8TjCPvJPUD4SUcQXRHOL7wRNsaKqu8UZTuxoyzTSeF586QIa/liGD80pI3SF7ix1AXyK40fCshDrxh9RtJEM+zW5Av3wh/Y4jOul6jot/B9MbONPnhmU4ro0fGVkMUkwobZwd25kf6ruQzzDOu+PC+RoOKXnA8okPDmYTHowJiYwGPuiSRQ3fn/8yDdchzB2+XSfHjjSzLWeGC0cL1SfuJZYVGcYyTLOLTGT75wqQ0yUo43G8VugFpo4obVlQ22Us9AZtF65KiT4iXommXSGk3WUC9FxEz7M0yANQzCcUhW9KzkKOnM/Sr5ysRHalFjZ0oKURoCUkDdwZ+Xjr+atjQXeJ7Jci0NXQfK5aZKFNw4IRLe0fVhMYTZDzJwWcdcOomIfR38Phqvvb7Y8ef9zfq+edlfxteRqpIWtxNCQp0BKxzhOcsyO0CoCGVl6oBq3JhaKJaDJx4K7w60lIvtSWr8ajgR7FeQ92X25PlRy93iDLcBGBA8wyqon5CNPlJEWRWEPcmcT+ePNFNF+1tV2Jjpbd/DrhnVYCOB+jVNs9I7omHrGQBaOxS4O8Tf+cAMciDZKrm3bhbGCeD+3evHf1c+gNOxO970ZgUpV0TzHtbkO4+MnV99SEBaE+p4SueZquOUFhk2/B/Jyem5nP7TRdkqnp+EPwfE2yVtkqMzQbZ6FaTAVirGovU59TTrVAYhI9wp6Ov/ASFnX4PkD6w79M7eIOG1b4oYnYKdUfgrsoUMWxrbMgH725eaa55E7OiVg2sF3VV8fQHCDH8FUwoTdGLlYgUhPOTGNKDrgooyMgzyViqhtbeqUioLoaIHIiTZBAyekBAkMNaDelBUh5LcDDYt0ir8clYtpzDGuK/14Pb87KDKpRbFEtvLFpaEch4TlFTAMuhh3Ar0Fc/9sA2gZ+v9COYDijMkqcCBKoTR055wEAeRDmdwJhuSIIS5vqD21OqEV9kDo0VfPiQdUH5ktsF2LczkbSowGXBMgSk4VoH3OoE4jJ84yuumoRJ42JF4no35fqYkEbXBUsq2NyUuxLu2YwB9v7xG3KHRIv1KNdSzJPgz53fOwhH0gF6OIlJytvowl0HANZLI75lKoAByTaJ7BPXYApPziWh+uwmMIx7Rm4MtznlAGFv/5MHUowhnyhBHtwVkeqDE5iS4jnZrWC7UGrkwctpktgT55VsU1TopQ1/4MHqsOz6jycw8upYP1Sb+bff3uvF2fzNwb49NDSMpGqFyqejjaDc0jkbOBUsuEb0nxXa6nbf1STWlqLVyArPBmEntCsKLbTNMu1gKisU2VjNmkrcopPQEvpiOW8gr9kqb15FmDSxqZyG+v4Midu5V3IC25BaoHUP1qMtyuK8U2dFm0h4ybgO2ytoGC+Ipd7IdqyVDZ/L26I67vuhLv5MX/tT4C60q8mtvLEcEOMsBDH6sGl5wRKgvEWGKUv8J2wvu/sy7N77xqfC3SkeXPov+mSMOBRw8E2vr6E6QWpU104FjH8cxqJqgUcmwcKJvXwG+NEhws5K9HA6oKNrYCiIV6zhtcPg18vl98G8kyZq7eq1DuLHLbHRPPb+7cTmlcC/04i6BRpe7oHLU4Ssv4jsFbqNk7+d6cXf7zL6QhIb2/fz93F6Onh+rSqmKxM21VNCSZ55aF8CF896PjErWnY1CPPhJeR5filtLUvc8/1/PSM+/7o39jJaPFWCXb71h5NaWpo32nQ8pclZ/jNDV0PyBw6xl917F+PrF0or/HF8Y3hH8xTbX51y3guZyLGGUXhj56kk3YKsIuTBZkCmXo8ebG6FV41iro174Do5Cgml10RhvVu/bDeR86nwKrJix9kbySfIhuPB867pQVuGC3wtiB7qlqYxoPMlEs/6SKJ7Xfzl9RsqJOYAOHDyyz/grmffTWHoTSrKzFDV2Fnop0wvOxtrQPHk3rcMIJIa8+szAyRZ7hhmsvMyEe6dxbWjbhhthdqEcrsP7Vsp4WJ9DiWWLvEJGdMDMgwN2iGcZgcdu04HmFNvA4kjSFhuO2yGI9RJpbS+s4yEatmnY5CC696ZKLYZgna5iVixCSbhKyKRVcsFIcwZE8ejKD9FsghfO88uPg63EyuFoiiYxO3zaYv3kRxPhAE9gKvRJSr+I/jc4FrsD0RX3S4gJYehNj9nDrHWywwWpOUyLY6JxLSGjuL1C8b0bnnyEwuSTAgOvBvrWJ4P/mRSme9GN5zguQRhkMOE2KNclbWSuuqLpgdAZnjaP9KXPawp0TwKGuiiVyCC8sXeHEOo4Mh2tBYWsUE2SdUTvSajILdcwn2qEGwCbacTeswq0FHos1qvhYVIuKMyM/Y3psOkc/v6qNAWo0CysKfBjZ6M3SfRr6MBnnpgV+rSy87LQpCP11aboRDEXmYcOKWEQdLItHAqavOzP+ShUQh+h7eL6NWafPQPNfKeHDzkXmmMMrGTeuTRYdK0TzTNHwFQspBjZX9EbIz0pSow4nqmS4TT7bJ2aAZeY8pG/pq1PmzMsWXB5OfPkwHOnh6YXjN9upnPagLgnj680EIfyR1zuapm0k0QZSj6ELs/Y9CaruH0eRaCvfKYgG1JYzuKP06GRdbJ5seWpBekr9QJoFeqFimsoU0LXuvh/VRg91t3Rqtw4lE/mPxU9v/Fz5Aajz4Vw8etzMloQrLalSashrZVVzraWQUrZHvKUDuLC64ixwxq9ibF60ni3A+zTLyWakqMZZRYiyj1NQq5k3/PpxCkmn2kXVjqf61UgBjqcSGVGPmUhUb4wkoI+s0sqcp3gsKWM3IIho0zmcHKTZWlEOzU7H1khWcB16f/cQcs+vDklpNrQ878zsf2Mq91oPr+1RC7N5y0srystcq2KF1uP/wJXuV87rJuaq9eKTQiMQuxqrDrpc+eOiwnGjriB1y0o4pw7X68sqILp0lHy9akaVR6BzyrYpliC3ErrCQPVdN/1SwWXjND0QRxCZa7BDqhhNexTBhgTlC+4mKbr6yScSviSNu9RayFxutuf9afsRFdtDIKN3HIDAmQbpusOAe1ll6kQtOezcNDNW2sF5AjgNQ74/jgditeBf43iK9N+RgiaWEbixl63bpCoDODWh3+GoyV2lKJ2Jl+ZQ5E0AXvxIVv2+/urRnqFNbnRczT6mKtLvVsh7319staSqpSfMlBd+0KZlbZbEp64QtpApXub6mZzSZxHkDwf8jz8LnaoiWuCo2TkjbrffOrrlEmVodsTkPZulzboA6YUe9j5xv2+1fk1e8WJiGFia9NpML1I2bXPWDJlf0b/qNzYJaJvolpzNowdng+gPatNk8Wl7eZwIPE3uLvQ67Y2/QeuokajKFL6W6XjWADad+oM4ObLRNJSIBST4RqOJUEFerrilOJXPTRdygRZo0rS3kzkXZGJG3SuKgrJ3V8aYnl911GbaHdOvnG+4X0QujU9ukXCaN30aDf+g0KVb1AjJNqLlBUxbXNrSkbNxE00/QTVISpT1iLTMRIR8wOTRo8WzQioP7HJeuiccwtEOFLGhJUlwXWCUZc3t/ZFkg/3jxikkzDaZRDmy0eq8kjeluZUeZDMnuHlbKhvXkQsp0LmR4paPwkJaxQe0FE/h1OmGiJJOqPn6JxO3V9e3zSZR/8PVsETxp+B266xhka1F+pbpw6bfHCPKZg6S1oB6CtpEmNiKaQxASRPiQoQfFigAz29yw8IXI7Pv+qPUBzSdJWm8+xd6s/ZhP+XhDN1CkYr2JDa+Oq+PlCZbm2+juNiKdo44nghbPUbX+RCGIBzaR4sWGjcJtnj14PDZMqnVvvPHErm2nM40y5MM2yigL4ZYZVUkYDkTx1GKBsEOYqxzCx+MgDMc0rW5IErffZj8dldvW/fBxwkj121rtjpy3tViBo+zuFLWpzSwt57hNra2a9Mn1V7cdl+3ExkRFkFoy3YSDFwHVhgxkyoeqp08rLSX1j25+JADQ6lSC3y2glUnIDbbkrW7/pQocPrrxFwHDpzf+5s895V59/vY8unzWzYunb7++jRiR1pHf92TdxiSEGsiJrAKY6XDQPVb1tVr5S1HC3NqdJxL0liktthS+qfZdmqliS06LhsMOXtvV5FSC973UwsQpY1y7vSNPREUMHM1pW5fH5BpHHrYNu9f7+sQbuqKo7+abolsr4dRsMh8isU5jL38F/I/mfqZwHCdle8XyFbk9Xe+W6G0bxOG218Oas1f5drsWRuUzfQfaUTXopowqZJw9UYfXAzHq9/7qtX/talP7Ru51Qde5ebGYzvn9k31RswtuDT2z+kiy7ZFygVaJnJnvr2qHq8r9jXe1KxQ+U9TeyZYTVCVfDmS2C+tk65vrMxyGk8m7yRnjh+vFk/cseKsf7oAhRMgWHCepPTeiXjy+/zoL6xacItcIOgmHrXnIStmqNUm6IWVP8n3g+zcF8gRYyq01pMJzgKdzKHdto1UrVn27e73Qte+rx5/yy92fM6//905iKhXZpbL0DBxQVXOp1SPjA9UlSZD+CA8Epu5dnMtaWhW5ls9TfqWyGqBde+jk0CDrr6qOBnlkaW5n1jKsba5RGoGXQzZK+6/mOyXIMp3CmNVzjSW/3F6Yv4ePZ+Lq18+n5+nkeobOwf7xh/vw+KNkGg1vDT/4QiV7NGRVnPqAiEkIfI4Qwv2N7qiOu7VWa+2cGVAOLlqypbVfkzSK90wohbZ1zYA+VN18HQVISsY502agVlOVKeyNVQqFZiy+gq13wIjstUO06ahfaK55bTDvzHVtY+z5evUw3o+n5LwykdmgbMzfQDLi3yrJIwmfSU09U7znECPH1Ck5j9qbiHHuV+QEsVn/ZU0Nd+aNW5qf7RJkqarW+3IG7cy+20s43t5l9XHlv5RTNznATJaRo2mUEpBNMaiNn4qTz2LaJtkwp318yK/lIwj4MXkjeQ4tfg4/rwD//UIO/suk81lE54X03Mqf9DDs++nayWw1+N2JMfVs/3LJ86mmiZanpkH6n0VjjGt+fiZRMp2ymCzSnJbMFjhVmkzs2UyPfYIntXoEd9vJbD8auCTmcXTbyBMHlGzLzPzF6tO9ma+XtYEIw193BNt6lSTKCFSqTFRGtfX2RUG7wiTqqyhrA0GH2eVvT50IJRm0ZNq7KYlcS9iW6vkMyvJZ8GyY5vPJicRpZb7BtJDOl0P5MjveQO+xu/WWmQfvCF2KBDU4w3kgC9QSR2bTzvss0ZtKnEpRh99aveWlcihFNUnDwf93M51lXYgs1YlhW9RhqpdtbZhasEG8FeZ3SO5+FDWOOWQjoDe/0fJ/vX626vWT3dz0M/T5qVAMW3ufn4zhI1ftfVhHurYnIflSW74ajwZ6FOc92H25ZfjZErQlyYWGpaF5UPCE8TEPwftILto4Hy9vZxWu3QaJOkCS+VrEa7ahLFexzUqVoHVeDZSFrMjI/V59G6gzaSwvIGrLqrtO2gH5BXrIc6coTa7NpMjtyfSqhWrbbbkWqmURkrRlrj+//lL7TTKzuXuXX14Mp+D+Hv0VL1yZu2dg413UDfu/3UmKSK4jFeZb7ENDfXy50kfTxeDnC1x9n4ykwR/mQh5Ejq2vSUqcIwpPd2xoARA3sdmm3hO0OhKdPqsoaxrZFNZ7ZtBnD4VNtbta5I7SkjMVRjLX4tMFSFumz8oK18r0+RP5Titzi/W5XpgU36DILS1eSpekHHnBUmUHDE3fxQBTKoWPqGAJf7URWdJkOpYasxukk0W4+B8=7VxZe9q4Gv41PG0v6OMFG3JJQtJ0mkyY5PR0pjd9hC1ArW25stj6648kS3iTWYIhZHrai9iyrOXb3m+RadlX4fIDAfH0HvswaFmGv2zZg5ZlmWbPZX94yypt6fE73jAhyJedsoYn9AvKRkO2zpAPk0JHinFAUVxs9HAUQY8W2gAheFHsNsZBcdYYTGCl4ckDQbX1C/LpVO2im7XfQjSZqplN9yJ9EgLVWe4kmQIfL3JN9nXLviIY0/QqXF7BgBNP0SV976bm6XphBEZ0lxeir8RMvi3ny3nv9sOl9+lv6odt00mHmYNgJnd8DyJOE0bSAPGhLWPwcZDIPdCVIgz0GZ3kLSZ0iic4AsF11npJ8CzyIZ/dYHdZnzuMY9ZossbvkNKVZDqYUcyapjQM5NMxjqh8aPIh2GrI6m85nrj5h9+8d9TtYJl/OFjJuyqlJPESPCMe3EAeSR0KyATSTWS0046cKLkZJCM+QBxCtiDWQeqF8d7oKkFZFQWewABQNC+KH5BSPFmPtB58iJFgkhzZujAKw9odozhEuhf5Vl5c1ECqIx6PE1jowy5ym8mahKDtIXRVmWtZbsDoezliFxN+cTVLKJuHsG59P0QRSihhZMHRuidRXVULW8qo3MYlqCC27s8ZVg/aiZCtPutgGvEyfU0+Xw/kXPL3VwmFIbtoOYPcdOnoxRmr68paPidMGyxjxAXhaSgsGae33J/YLKIJ1zy5e3791mNyy/5iMgER+iWIkLyrqGNR2RZTROFTDIRwL5hlLikWCoIrHGAi3rWh6Tuwy9oZkfEPmHty4XZt4G5SoTkkFC53kXm7KJhrQV1kZtXsybZp3qQaRr0SFERzXzk0rYog9vPM4Dwq8zAmMIGRJzgScRZO+eWfM8G3CNIFJj80siDEyMNhCCIuAgGKoBKn09vVzI6+N+2Lgi3dYknF3RASxBgAycHmtdu0eT3QeNquU5RRezfj+QzD+J/P9BfyyPKrhYA9h6Prx9G3dlUezwF2U6ugvJ+CAJkF4XlvbUPi77MwVqsHxGtaeIKf3zvG5+Ffn6BxA+/bt9fOw03bPI3sWN2ifTOt3WSnTwhY5brFvEOyYR5bP08Fx2v6dyynJLrpChpF+G4twicxiLSIPALej4mQ3baXAhAHZjIZvbUctmK2DiN/8U4L18J8sxU/sOhgk6OQrkI1l1SMOeoxv5yFQd+jfCWXHOUQCwruwAgGQ5wg4YrYgxGmDKhzHfoBmvAHFJcgF88oN/pX6zBlo+XcA1WdIns1qGprQNU9GqYaVUydMZCMOHkoB71ZgqIJ+/vx8b5fh5Qgjs8CHrtOaw90fL4t6x0tzujYrttIYNEp2Terd36BhdmrtTs7RwJmTSTwXwQXbFEFbz2EPlf6qhT7gII62R7xpAR//SYn5RtsVTHKeHl3IKce56Yd7r7a4Vh2rxHtcLrWs9D/lNpRrxzHBeWrNICNCR7DJEFc+H4TcO52i/HEi4OzXSsC69SJDGX/xD58zbmWckTOxo28VFA1CRU2gUjIZNG96Eigzx0XLq83wPOY9HIzxIIhri2hyJOGUCp0U8kYH8De2NMlY1yvB0fjhtzGztllYzo6+/SycGdqo9/nA5kCqBdIeBzGmosKax7hzxlMmOjfMKFDYwQ1AVdRZRKhVOU+H5NkxrWRMWJaUDl9xUHY+CLDgLTqHnsP5vFAmfsQ+X4qG5BZITAS43HpkKE2G5xZHmZtqsyvsnqz6Jb1b12XkrO28qWfmsJAr1csDLTtwzyUE7jd7v5ut8b8l5HoEUqFsownSObC4+Zrt4zr28f9sKkGQM4Lsu5VNGGk4YOBxy1VhXuTCDmk/FqjaW99GMPITwNrkZdOZnHMrB8vOLgg5JATjZI4Ny9S717iIBuSLXv94DUWGbrG2eGapSt3/Y5hnNr3VvRzmgY/fcxmmifL9uvp8QyzuWu24joSiLOuTCXMgCJPB9JjzO1qruK4JWtxeL7ihBiuF9jNanowhrc5iDvdomwdhuFq5FKSwS0OcDyEV+s/fULf89gInATpsRTym2QOTOO8MgdWNTi7w6mz8pSaFnY1JHiOfGEk3j4Nq+4D2zstkq/oC0Q4giXHQTYp+xDAMd1kHXQuSRE5fZBMxU0ZE211L9erOV916JEHZ0dfpIxDjXHRrj9501B+PEuJKydWeOxlnY3gBLMgT9SDpFfLHQMConHm5pdfgtR7hWnxdUWp7kjFEtHca+zun9yT7CV+c7gPZh8vl64A9ECM65br1RfNZdIrk/V6JQ0tp+1Tgh7NBewc0QWsz4CWFYuoXI5RydgYwj2shJuYTuUJqTfvMu+S75b7b8DLn9Q7M121D9DVhMke7fNTxdxXDUCSIE8136BATXeAejZ+Iqpp9eyU1dM5v0KXXX/85HWVORpMv4wd/l+XfnHFP7mBXHv6rxl3tlxucLovnpaxn3FWYL+kJfeIXnnS8l8jgOW84BkIYKdajH1QsPr/kGrXkKpT5eP6i5uTxFQdHdiUXZzIV16DpH6BZjnu8fYhoMyJilKCGXarXIO0KhwoE72WzlvdhRwZHQ0VVduh56yf53Xv774fOYOryLHJ0XhSdlZUczLHWW/5/zUGt8TirkZPzU0S1rieOmaFV3dwAvnnGK/ZpjpHsKm6mlnnlDbV2eWcEo7aqrhRULH0i5xAZSeHawdMe+7nyEr4Ip9Wub0TKlv39uF6OgzcPvz68IV8tT5NV3+0q7pWF4Bt492R+XOi01Yl/lgn5M/H9uD2C1n2/vgcmzf2l2/2wpi3dfp1VJ/FfPU+i2s25LRUBjqaz6Jl/e6qmeZGjLUDcwciP/F4Ec0yBghMCAh30s/D8XR7WXg7oublrSGza5WCSrOq1p1j4aaWt50Kb+XZ6wcN5lWTELWlXJFZWMgd8NxChEkIgo0ZkYbSFF72IbhAiBTPt56z2lYMflU+XhNx8xpzNtQijyWr2m9dq7JahaA9KwPVr0uP+/Vr/oPXXcoHtWzM1wU2fcN6Ph9Kl6Rp1ypAUx9Ka361BEeIiqNMCQV0lvBh2STI41cBnrz4cWK9AGxUjSZOIlnmxYGFn0brNZuke5NLItkrDrgepVpTV0VtbqDDjwpv7SiplAicfFM896ep+3o4CJg08X5rXTHkD1ToNObVBcIdQ4NyTWX52W32402pKmQ/gWVf/w8= \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From dc62b9699ff03bfba616daf5a75dd3c1caddf633 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Thu, 11 Jan 2024 14:21:46 +0100 Subject: [PATCH 07/15] Update discovery/interface.go Co-authored-by: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> --- discovery/interface.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery/interface.go b/discovery/interface.go index e81294c94f..ed731ea9aa 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -105,7 +105,7 @@ type Client interface { // Register causes a DID, in the form of a Verifiable Presentation, to be registered on a Discovery Service. // Registration will be attempted immediately, and automatically refreshed. - // If initial registration failed, it will return ErrRegistrationFailed, but it will keep retrying. + // If the initial registration fails with ErrRegistrationFailed, registration will be retried. // If the function is called again for the same service/DID combination, it will try to refresh the registration. // It returns an error if the service or DID is invalid/unknown. Register(ctx context.Context, serviceID string, subjectDID did.DID) error From 49cf928be490d10f9c63e1176e3c12c28995ac78 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Thu, 11 Jan 2024 14:15:39 +0100 Subject: [PATCH 08/15] pr feedback --- discovery/api/v1/wrapper.go | 2 +- discovery/api/v1/wrapper_test.go | 10 +++++----- discovery/client.go | 8 ++++---- discovery/client_test.go | 6 +++--- discovery/interface.go | 6 +++--- discovery/module.go | 2 +- discovery/module_test.go | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/discovery/api/v1/wrapper.go b/discovery/api/v1/wrapper.go index 9c8f1212f4..9a4fe87af1 100644 --- a/discovery/api/v1/wrapper.go +++ b/discovery/api/v1/wrapper.go @@ -111,7 +111,7 @@ func (w *Wrapper) RegisterDID(ctx context.Context, request RegisterDIDRequestObj return nil, err } err = w.Client.Register(ctx, request.ServiceID, *subjectDID) - if errors.Is(err, discovery.ErrRegistrationFailed) { + if errors.Is(err, discovery.ErrPresentationRegistrationFailed) { // registration failed, but will be retried return RegisterDID202JSONResponse{ Reason: err.Error(), diff --git a/discovery/api/v1/wrapper_test.go b/discovery/api/v1/wrapper_test.go index fa6262948b..cc42fd78c3 100644 --- a/discovery/api/v1/wrapper_test.go +++ b/discovery/api/v1/wrapper_test.go @@ -109,7 +109,7 @@ func TestWrapper_RegisterDID(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().StartRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) + test.client.EXPECT().Register(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) response, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ ServiceID: serviceID, @@ -122,7 +122,7 @@ func TestWrapper_RegisterDID(t *testing.T) { t.Run("ok, but registration failed", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().StartRegistration(gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrRegistrationFailed) + test.client.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrPresentationRegistrationFailed) response, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ ServiceID: serviceID, @@ -135,7 +135,7 @@ func TestWrapper_RegisterDID(t *testing.T) { t.Run("other error", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().StartRegistration(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo")) + test.client.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo")) _, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ ServiceID: serviceID, @@ -150,7 +150,7 @@ func TestWrapper_DeregisterDID(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) + test.client.EXPECT().Deregister(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) response, err := test.wrapper.DeregisterDID(nil, DeregisterDIDRequestObject{ ServiceID: serviceID, @@ -163,7 +163,7 @@ func TestWrapper_DeregisterDID(t *testing.T) { t.Run("error", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().StopRegistration(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(errors.New("foo")) + test.client.EXPECT().Deregister(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(errors.New("foo")) _, err := test.wrapper.DeregisterDID(nil, DeregisterDIDRequestObject{ ServiceID: serviceID, diff --git a/discovery/client.go b/discovery/client.go index 0352ac2509..7a52af7c92 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -80,7 +80,7 @@ func (r *scheduledRegistrationManager) register(ctx context.Context, serviceID s // retry registration asap var next time.Time _ = r.store.updateDIDRegistrationTime(serviceID, subjectDID, &next) - return errors.Join(ErrRegistrationFailed, err) + return errors.Join(ErrPresentationRegistrationFailed, err) } log.Logger().Debugf("Successfully refreshed Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) return nil @@ -98,7 +98,7 @@ func (r *scheduledRegistrationManager) deregister(ctx context.Context, serviceID "credentialSubject.id": subjectDID.String(), }) if err != nil { - return errors.Join(ErrRegistrationFailed, err) + return errors.Join(ErrPresentationRegistrationFailed, err) } if len(presentations) == 0 { // no registration, nothing to do @@ -110,11 +110,11 @@ func (r *scheduledRegistrationManager) deregister(ctx context.Context, serviceID "retract_jti": presentations[0].ID.String(), }) if err != nil { - return errors.Join(ErrRegistrationFailed, err) + return errors.Join(ErrPresentationRegistrationFailed, err) } err = r.client.Register(ctx, service.Endpoint, *presentation) if err != nil { - return errors.Join(ErrRegistrationFailed, err) + return errors.Join(ErrPresentationRegistrationFailed, err) } return nil } diff --git a/discovery/client_test.go b/discovery/client_test.go index b7882bbd63..a737d06ce5 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -51,7 +51,7 @@ func Test_scheduledRegistrationManager_register(t *testing.T) { err := manager.register(audit.TestContext(), testServiceID, aliceDID) - require.ErrorIs(t, err, ErrRegistrationFailed) + require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "invoker error") }) t.Run("no matching credentials", func(t *testing.T) { @@ -66,7 +66,7 @@ func Test_scheduledRegistrationManager_register(t *testing.T) { err := manager.register(audit.TestContext(), testServiceID, aliceDID) - require.ErrorIs(t, err, ErrRegistrationFailed) + require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "DID wallet does not have credentials required for registration on Discovery Service (service=usecase_v1, did=did:example:alice)") }) t.Run("unknown service", func(t *testing.T) { @@ -129,7 +129,7 @@ func Test_scheduledRegistrationManager_deregister(t *testing.T) { err := manager.deregister(audit.TestContext(), testServiceID, aliceDID) - require.ErrorIs(t, err, ErrRegistrationFailed) + require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "remote error") }) } diff --git a/discovery/interface.go b/discovery/interface.go index ed731ea9aa..350bb3916c 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -85,8 +85,8 @@ var ErrServiceNotFound = errors.New("discovery service not found") // but a presentation with this ID already exists. var ErrPresentationAlreadyExists = errors.New("presentation already exists") -// ErrRegistrationFailed indicates registration of a presentation on a remote Discovery Service failed. -var ErrRegistrationFailed = errors.New("registration failed") +// 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") // Server defines the API for Discovery Servers. type Server interface { @@ -105,7 +105,7 @@ type Client interface { // Register causes a DID, in the form of a Verifiable Presentation, to be registered on a Discovery Service. // Registration will be attempted immediately, and automatically refreshed. - // If the initial registration fails with ErrRegistrationFailed, registration will be retried. + // If the initial registration fails with ErrPresentationRegistrationFailed, registration will be retried. // If the function is called again for the same service/DID combination, it will try to refresh the registration. // It returns an error if the service or DID is invalid/unknown. Register(ctx context.Context, serviceID string, subjectDID did.DID) error diff --git a/discovery/module.go b/discovery/module.go index ce1713634f..a2599fceff 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -267,7 +267,7 @@ func (m *Module) Register(ctx context.Context, serviceID string, subjectDID did. return errors.New("not owner of DID") } err = m.registrationManager.register(ctx, serviceID, subjectDID) - if errors.Is(err, ErrRegistrationFailed) { + if errors.Is(err, ErrPresentationRegistrationFailed) { log.Logger().WithError(err).Warnf("Discovery Service registration failed, will be retried later (did=%s,service=%s)", subjectDID, serviceID) } else if err == nil { log.Logger().Infof("Successfully registered Discovery Service (did=%s,service=%s)", subjectDID, serviceID) diff --git a/discovery/module_test.go b/discovery/module_test.go index d1d4201d4b..849128c347 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -342,6 +342,6 @@ func TestModule_Register(t *testing.T) { err := m.Register(context.Background(), testServiceID, aliceDID) - require.ErrorIs(t, err, ErrRegistrationFailed) + require.ErrorIs(t, err, ErrPresentationRegistrationFailed) }) } From c54293e3e4078ac287de890ceaa973a5957ceaf7 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Thu, 11 Jan 2024 16:08:25 +0100 Subject: [PATCH 09/15] godoc --- discovery/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/discovery/config.go b/discovery/config.go index 15d3cc7d56..5f18d02556 100644 --- a/discovery/config.go +++ b/discovery/config.go @@ -41,6 +41,7 @@ type ServerConfig struct { // ClientConfig holds the config for the client type ClientConfig struct { // RegistrationRefreshInterval specifies how often the client should refresh its registrations on Discovery Services. + // At the same interval, failed registrations are refreshed. RegistrationRefreshInterval time.Duration `koanf:"registration_refresh_interval"` } From d643435b06d0a76ee448562ca07403e983e986d5 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 12 Jan 2024 10:20:50 +0100 Subject: [PATCH 10/15] Renamed lots of stuff --- auth/api/iam/generated.go | 2 +- discovery/api/v1/{wrapper.go => api.go} | 22 +- .../api/v1/{wrapper_test.go => api_test.go} | 34 +-- discovery/api/v1/generated.go | 194 +++++++++--------- discovery/client.go | 36 ++-- discovery/client_test.go | 20 +- discovery/interface.go | 12 +- discovery/mock.go | 52 ++--- discovery/module.go | 34 +-- discovery/module_test.go | 44 ++-- discovery/store.go | 32 +-- discovery/store_test.go | 18 +- docs/_static/discovery/v1.yaml | 16 +- ...3_discoveryservice_client_registration.sql | 16 +- 14 files changed, 272 insertions(+), 260 deletions(-) rename discovery/api/v1/{wrapper.go => api.go} (80%) rename discovery/api/v1/{wrapper_test.go => api_test.go} (81%) diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index eb46a661b8..f7badfa4a8 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -119,7 +119,7 @@ type RequestAccessTokenJSONBody struct { // RedirectURL The URL to which the user-agent will be redirected after the authorization request. RedirectURL *string `json:"redirectURL,omitempty"` - // Scope The scope that will be The service for which this access token can be used. + // Scope The scope that will be the service for which this access token can be used. Scope string `json:"scope"` // UserID The ID of the user for which this access token is requested. diff --git a/discovery/api/v1/wrapper.go b/discovery/api/v1/api.go similarity index 80% rename from discovery/api/v1/wrapper.go rename to discovery/api/v1/api.go index 9a4fe87af1..023ce014d7 100644 --- a/discovery/api/v1/wrapper.go +++ b/discovery/api/v1/api.go @@ -82,7 +82,7 @@ func (w *Wrapper) GetPresentations(_ context.Context, request GetPresentationsRe } func (w *Wrapper) RegisterPresentation(_ context.Context, request RegisterPresentationRequestObject) (RegisterPresentationResponseObject, error) { - err := w.Server.Add(request.ServiceID, *request.Body) + err := w.Server.Register(request.ServiceID, *request.Body) if err != nil { return nil, err } @@ -105,15 +105,15 @@ func (w *Wrapper) SearchPresentations(_ context.Context, request SearchPresentat return SearchPresentations200JSONResponse(result), nil } -func (w *Wrapper) RegisterDID(ctx context.Context, request RegisterDIDRequestObject) (RegisterDIDResponseObject, error) { +func (w *Wrapper) ActivateServiceForDID(ctx context.Context, request ActivateServiceForDIDRequestObject) (ActivateServiceForDIDResponseObject, error) { subjectDID, err := did.ParseDID(request.Did) if err != nil { return nil, err } - err = w.Client.Register(ctx, request.ServiceID, *subjectDID) + err = w.Client.ActivateServiceForDID(ctx, request.ServiceID, *subjectDID) if errors.Is(err, discovery.ErrPresentationRegistrationFailed) { // registration failed, but will be retried - return RegisterDID202JSONResponse{ + return ActivateServiceForDID202JSONResponse{ Reason: err.Error(), }, nil } @@ -121,17 +121,23 @@ func (w *Wrapper) RegisterDID(ctx context.Context, request RegisterDIDRequestObj // other error return nil, err } - return RegisterDID200Response{}, nil + return ActivateServiceForDID200Response{}, nil } -func (w *Wrapper) DeregisterDID(ctx context.Context, request DeregisterDIDRequestObject) (DeregisterDIDResponseObject, error) { +func (w *Wrapper) DeactivateServiceForDID(ctx context.Context, request DeactivateServiceForDIDRequestObject) (DeactivateServiceForDIDResponseObject, error) { subjectDID, err := did.ParseDID(request.Did) if err != nil { return nil, err } - err = w.Client.Deregister(ctx, request.ServiceID, *subjectDID) + err = w.Client.DeactivateServiceForDID(ctx, request.ServiceID, *subjectDID) + if errors.Is(err, discovery.ErrPresentationRegistrationFailed) { + // deactivation succeeded, but Verifiable Presentation couldn't be removed from remote Discovery Server. + return DeactivateServiceForDID202JSONResponse{ + Reason: err.Error(), + }, nil + } if err != nil { return nil, err } - return DeregisterDID200Response{}, nil + return DeactivateServiceForDID200Response{}, nil } diff --git a/discovery/api/v1/wrapper_test.go b/discovery/api/v1/api_test.go similarity index 81% rename from discovery/api/v1/wrapper_test.go rename to discovery/api/v1/api_test.go index cc42fd78c3..a4b0cdf19d 100644 --- a/discovery/api/v1/wrapper_test.go +++ b/discovery/api/v1/api_test.go @@ -81,7 +81,7 @@ func TestWrapper_RegisterPresentation(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) presentation := vc.VerifiablePresentation{} - test.server.EXPECT().Add(serviceID, presentation).Return(nil) + test.server.EXPECT().Register(serviceID, presentation).Return(nil) response, err := test.wrapper.RegisterPresentation(nil, RegisterPresentationRequestObject{ ServiceID: serviceID, @@ -94,7 +94,7 @@ func TestWrapper_RegisterPresentation(t *testing.T) { t.Run("error", func(t *testing.T) { test := newMockContext(t) presentation := vc.VerifiablePresentation{} - test.server.EXPECT().Add(serviceID, presentation).Return(discovery.ErrInvalidPresentation) + test.server.EXPECT().Register(serviceID, presentation).Return(discovery.ErrInvalidPresentation) _, err := test.wrapper.RegisterPresentation(nil, RegisterPresentationRequestObject{ ServiceID: serviceID, @@ -105,39 +105,39 @@ func TestWrapper_RegisterPresentation(t *testing.T) { }) } -func TestWrapper_RegisterDID(t *testing.T) { +func TestWrapper_ActivateServiceForDID(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().Register(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) + test.client.EXPECT().ActivateServiceForDID(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) - response, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ + response, err := test.wrapper.ActivateServiceForDID(nil, ActivateServiceForDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) assert.NoError(t, err) - assert.IsType(t, RegisterDID200Response{}, response) + assert.IsType(t, ActivateServiceForDID200Response{}, response) }) t.Run("ok, but registration failed", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrPresentationRegistrationFailed) + test.client.EXPECT().ActivateServiceForDID(gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrPresentationRegistrationFailed) - response, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ + response, err := test.wrapper.ActivateServiceForDID(nil, ActivateServiceForDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) assert.NoError(t, err) - assert.IsType(t, RegisterDID202JSONResponse{}, response) + assert.IsType(t, ActivateServiceForDID202JSONResponse{}, response) }) t.Run("other error", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo")) + test.client.EXPECT().ActivateServiceForDID(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo")) - _, err := test.wrapper.RegisterDID(nil, RegisterDIDRequestObject{ + _, err := test.wrapper.ActivateServiceForDID(nil, ActivateServiceForDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) @@ -146,26 +146,26 @@ func TestWrapper_RegisterDID(t *testing.T) { }) } -func TestWrapper_DeregisterDID(t *testing.T) { +func TestWrapper_DeactivateServiceForDID(t *testing.T) { t.Run("ok", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().Deregister(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) + test.client.EXPECT().DeactivateServiceForDID(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(nil) - response, err := test.wrapper.DeregisterDID(nil, DeregisterDIDRequestObject{ + response, err := test.wrapper.DeactivateServiceForDID(nil, DeactivateServiceForDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) assert.NoError(t, err) - assert.IsType(t, DeregisterDID200Response{}, response) + assert.IsType(t, DeactivateServiceForDID200Response{}, response) }) t.Run("error", func(t *testing.T) { test := newMockContext(t) expectedDID := "did:web:example.com" - test.client.EXPECT().Deregister(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(errors.New("foo")) + test.client.EXPECT().DeactivateServiceForDID(gomock.Any(), serviceID, did.MustParseDID(expectedDID)).Return(errors.New("foo")) - _, err := test.wrapper.DeregisterDID(nil, DeregisterDIDRequestObject{ + _, err := test.wrapper.DeactivateServiceForDID(nil, DeactivateServiceForDIDRequestObject{ ServiceID: serviceID, Did: expectedDID, }) diff --git a/discovery/api/v1/generated.go b/discovery/api/v1/generated.go index 6c6d8dc70f..b490c6b5fc 100644 --- a/discovery/api/v1/generated.go +++ b/discovery/api/v1/generated.go @@ -131,11 +131,11 @@ type ClientInterface interface { // SearchPresentations request SearchPresentations(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - // DeregisterDID request - DeregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeactivateServiceForDID request + DeactivateServiceForDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) - // RegisterDID request - RegisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) + // ActivateServiceForDID request + ActivateServiceForDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) GetPresentations(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -186,8 +186,8 @@ func (c *Client) SearchPresentations(ctx context.Context, serviceID string, para return c.Client.Do(req) } -func (c *Client) DeregisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewDeregisterDIDRequest(c.Server, serviceID, did) +func (c *Client) DeactivateServiceForDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeactivateServiceForDIDRequest(c.Server, serviceID, did) if err != nil { return nil, err } @@ -198,8 +198,8 @@ func (c *Client) DeregisterDID(ctx context.Context, serviceID string, did string return c.Client.Do(req) } -func (c *Client) RegisterDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewRegisterDIDRequest(c.Server, serviceID, did) +func (c *Client) ActivateServiceForDID(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewActivateServiceForDIDRequest(c.Server, serviceID, did) if err != nil { return nil, err } @@ -365,8 +365,8 @@ func NewSearchPresentationsRequest(server string, serviceID string, params *Sear return req, nil } -// NewDeregisterDIDRequest generates requests for DeregisterDID -func NewDeregisterDIDRequest(server string, serviceID string, did string) (*http.Request, error) { +// NewDeactivateServiceForDIDRequest generates requests for DeactivateServiceForDID +func NewDeactivateServiceForDIDRequest(server string, serviceID string, did string) (*http.Request, error) { var err error var pathParam0 string @@ -406,8 +406,8 @@ func NewDeregisterDIDRequest(server string, serviceID string, did string) (*http return req, nil } -// NewRegisterDIDRequest generates requests for RegisterDID -func NewRegisterDIDRequest(server string, serviceID string, did string) (*http.Request, error) { +// NewActivateServiceForDIDRequest generates requests for ActivateServiceForDID +func NewActivateServiceForDIDRequest(server string, serviceID string, did string) (*http.Request, error) { var err error var pathParam0 string @@ -501,11 +501,11 @@ type ClientWithResponsesInterface interface { // SearchPresentationsWithResponse request SearchPresentationsWithResponse(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*SearchPresentationsResponse, error) - // DeregisterDIDWithResponse request - DeregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*DeregisterDIDResponse, error) + // DeactivateServiceForDIDWithResponse request + DeactivateServiceForDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*DeactivateServiceForDIDResponse, error) - // RegisterDIDWithResponse request - RegisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*RegisterDIDResponse, error) + // ActivateServiceForDIDWithResponse request + ActivateServiceForDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*ActivateServiceForDIDResponse, error) } type GetPresentationsResponse struct { @@ -613,11 +613,11 @@ func (r SearchPresentationsResponse) StatusCode() int { return 0 } -type DeregisterDIDResponse struct { +type DeactivateServiceForDIDResponse struct { Body []byte HTTPResponse *http.Response JSON202 *struct { - // Reason Description of why registration deletion failed. + // Reason Description of why removal of the registration failed. Reason string `json:"reason"` } ApplicationproblemJSON400 *struct { @@ -643,7 +643,7 @@ type DeregisterDIDResponse struct { } // Status returns HTTPResponse.Status -func (r DeregisterDIDResponse) Status() string { +func (r DeactivateServiceForDIDResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -651,14 +651,14 @@ func (r DeregisterDIDResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r DeregisterDIDResponse) StatusCode() int { +func (r DeactivateServiceForDIDResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type RegisterDIDResponse struct { +type ActivateServiceForDIDResponse struct { Body []byte HTTPResponse *http.Response JSON202 *struct { @@ -688,7 +688,7 @@ type RegisterDIDResponse struct { } // Status returns HTTPResponse.Status -func (r RegisterDIDResponse) Status() string { +func (r ActivateServiceForDIDResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -696,7 +696,7 @@ func (r RegisterDIDResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r RegisterDIDResponse) StatusCode() int { +func (r ActivateServiceForDIDResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -738,22 +738,22 @@ func (c *ClientWithResponses) SearchPresentationsWithResponse(ctx context.Contex return ParseSearchPresentationsResponse(rsp) } -// DeregisterDIDWithResponse request returning *DeregisterDIDResponse -func (c *ClientWithResponses) DeregisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*DeregisterDIDResponse, error) { - rsp, err := c.DeregisterDID(ctx, serviceID, did, reqEditors...) +// DeactivateServiceForDIDWithResponse request returning *DeactivateServiceForDIDResponse +func (c *ClientWithResponses) DeactivateServiceForDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*DeactivateServiceForDIDResponse, error) { + rsp, err := c.DeactivateServiceForDID(ctx, serviceID, did, reqEditors...) if err != nil { return nil, err } - return ParseDeregisterDIDResponse(rsp) + return ParseDeactivateServiceForDIDResponse(rsp) } -// RegisterDIDWithResponse request returning *RegisterDIDResponse -func (c *ClientWithResponses) RegisterDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*RegisterDIDResponse, error) { - rsp, err := c.RegisterDID(ctx, serviceID, did, reqEditors...) +// ActivateServiceForDIDWithResponse request returning *ActivateServiceForDIDResponse +func (c *ClientWithResponses) ActivateServiceForDIDWithResponse(ctx context.Context, serviceID string, did string, reqEditors ...RequestEditorFn) (*ActivateServiceForDIDResponse, error) { + rsp, err := c.ActivateServiceForDID(ctx, serviceID, did, reqEditors...) if err != nil { return nil, err } - return ParseRegisterDIDResponse(rsp) + return ParseActivateServiceForDIDResponse(rsp) } // ParseGetPresentationsResponse parses an HTTP response from a GetPresentationsWithResponse call @@ -891,15 +891,15 @@ func ParseSearchPresentationsResponse(rsp *http.Response) (*SearchPresentationsR return response, nil } -// ParseDeregisterDIDResponse parses an HTTP response from a DeregisterDIDWithResponse call -func ParseDeregisterDIDResponse(rsp *http.Response) (*DeregisterDIDResponse, error) { +// ParseDeactivateServiceForDIDResponse parses an HTTP response from a DeactivateServiceForDIDWithResponse call +func ParseDeactivateServiceForDIDResponse(rsp *http.Response) (*DeactivateServiceForDIDResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeregisterDIDResponse{ + response := &DeactivateServiceForDIDResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -907,7 +907,7 @@ func ParseDeregisterDIDResponse(rsp *http.Response) (*DeregisterDIDResponse, err switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: var dest struct { - // Reason Description of why registration deletion failed. + // Reason Description of why removal of the registration failed. Reason string `json:"reason"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -952,15 +952,15 @@ func ParseDeregisterDIDResponse(rsp *http.Response) (*DeregisterDIDResponse, err return response, nil } -// ParseRegisterDIDResponse parses an HTTP response from a RegisterDIDWithResponse call -func ParseRegisterDIDResponse(rsp *http.Response) (*RegisterDIDResponse, error) { +// ParseActivateServiceForDIDResponse parses an HTTP response from a ActivateServiceForDIDWithResponse call +func ParseActivateServiceForDIDResponse(rsp *http.Response) (*ActivateServiceForDIDResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &RegisterDIDResponse{ + response := &ActivateServiceForDIDResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -1015,21 +1015,21 @@ func ParseRegisterDIDResponse(rsp *http.Response) (*RegisterDIDResponse, error) // ServerInterface represents all server handlers. type ServerInterface interface { - // Retrieves the presentations of a discovery service. + // Retrieves the presentations of a Discovery Service. // (GET /discovery/{serviceID}) GetPresentations(ctx echo.Context, serviceID string, params GetPresentationsParams) error - // Register a presentation on the discovery service. + // Register a presentation on the Discovery Service. // (POST /discovery/{serviceID}) RegisterPresentation(ctx echo.Context, serviceID string) error - // Searches for presentations registered on the discovery service. + // Searches for presentations registered on the Discovery Service. // (GET /discovery/{serviceID}/search) SearchPresentations(ctx echo.Context, serviceID string, params SearchPresentationsParams) error - // Client API to stop registering the given DID on the discovery service. + // Client API to unregister the given DID from the Discovery Service. // (DELETE /internal/discovery/v1/{serviceID}/{did}) - DeregisterDID(ctx echo.Context, serviceID string, did string) error - // Client API to start registering the given DID on the discovery service. + DeactivateServiceForDID(ctx echo.Context, serviceID string, did string) error + // Client API to activate a DID on the specified Discovery Service. // (POST /internal/discovery/v1/{serviceID}/{did}) - RegisterDID(ctx echo.Context, serviceID string, did string) error + ActivateServiceForDID(ctx echo.Context, serviceID string, did string) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -1109,8 +1109,8 @@ func (w *ServerInterfaceWrapper) SearchPresentations(ctx echo.Context) error { return err } -// DeregisterDID converts echo context to params. -func (w *ServerInterfaceWrapper) DeregisterDID(ctx echo.Context) error { +// DeactivateServiceForDID converts echo context to params. +func (w *ServerInterfaceWrapper) DeactivateServiceForDID(ctx echo.Context) error { var err error // ------------- Path parameter "serviceID" ------------- var serviceID string @@ -1131,12 +1131,12 @@ func (w *ServerInterfaceWrapper) DeregisterDID(ctx echo.Context) error { ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.DeregisterDID(ctx, serviceID, did) + err = w.Handler.DeactivateServiceForDID(ctx, serviceID, did) return err } -// RegisterDID converts echo context to params. -func (w *ServerInterfaceWrapper) RegisterDID(ctx echo.Context) error { +// ActivateServiceForDID converts echo context to params. +func (w *ServerInterfaceWrapper) ActivateServiceForDID(ctx echo.Context) error { var err error // ------------- Path parameter "serviceID" ------------- var serviceID string @@ -1157,7 +1157,7 @@ func (w *ServerInterfaceWrapper) RegisterDID(ctx echo.Context) error { ctx.Set(JwtBearerAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.RegisterDID(ctx, serviceID, did) + err = w.Handler.ActivateServiceForDID(ctx, serviceID, did) return err } @@ -1192,8 +1192,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/discovery/:serviceID", wrapper.GetPresentations) router.POST(baseURL+"/discovery/:serviceID", wrapper.RegisterPresentation) router.GET(baseURL+"/discovery/:serviceID/search", wrapper.SearchPresentations) - router.DELETE(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.DeregisterDID) - router.POST(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.RegisterDID) + router.DELETE(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.DeactivateServiceForDID) + router.POST(baseURL+"/internal/discovery/v1/:serviceID/:did", wrapper.ActivateServiceForDID) } @@ -1331,36 +1331,36 @@ func (response SearchPresentationsdefaultApplicationProblemPlusJSONResponse) Vis return json.NewEncoder(w).Encode(response.Body) } -type DeregisterDIDRequestObject struct { +type DeactivateServiceForDIDRequestObject struct { ServiceID string `json:"serviceID"` Did string `json:"did"` } -type DeregisterDIDResponseObject interface { - VisitDeregisterDIDResponse(w http.ResponseWriter) error +type DeactivateServiceForDIDResponseObject interface { + VisitDeactivateServiceForDIDResponse(w http.ResponseWriter) error } -type DeregisterDID200Response struct { +type DeactivateServiceForDID200Response struct { } -func (response DeregisterDID200Response) VisitDeregisterDIDResponse(w http.ResponseWriter) error { +func (response DeactivateServiceForDID200Response) VisitDeactivateServiceForDIDResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type DeregisterDID202JSONResponse struct { - // Reason Description of why registration deletion failed. +type DeactivateServiceForDID202JSONResponse struct { + // Reason Description of why removal of the registration failed. Reason string `json:"reason"` } -func (response DeregisterDID202JSONResponse) VisitDeregisterDIDResponse(w http.ResponseWriter) error { +func (response DeactivateServiceForDID202JSONResponse) VisitDeactivateServiceForDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(202) return json.NewEncoder(w).Encode(response) } -type DeregisterDID400ApplicationProblemPlusJSONResponse struct { +type DeactivateServiceForDID400ApplicationProblemPlusJSONResponse struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1371,14 +1371,14 @@ type DeregisterDID400ApplicationProblemPlusJSONResponse struct { Title string `json:"title"` } -func (response DeregisterDID400ApplicationProblemPlusJSONResponse) VisitDeregisterDIDResponse(w http.ResponseWriter) error { +func (response DeactivateServiceForDID400ApplicationProblemPlusJSONResponse) VisitDeactivateServiceForDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type DeregisterDIDdefaultApplicationProblemPlusJSONResponse struct { +type DeactivateServiceForDIDdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1392,43 +1392,43 @@ type DeregisterDIDdefaultApplicationProblemPlusJSONResponse struct { StatusCode int } -func (response DeregisterDIDdefaultApplicationProblemPlusJSONResponse) VisitDeregisterDIDResponse(w http.ResponseWriter) error { +func (response DeactivateServiceForDIDdefaultApplicationProblemPlusJSONResponse) VisitDeactivateServiceForDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) return json.NewEncoder(w).Encode(response.Body) } -type RegisterDIDRequestObject struct { +type ActivateServiceForDIDRequestObject struct { ServiceID string `json:"serviceID"` Did string `json:"did"` } -type RegisterDIDResponseObject interface { - VisitRegisterDIDResponse(w http.ResponseWriter) error +type ActivateServiceForDIDResponseObject interface { + VisitActivateServiceForDIDResponse(w http.ResponseWriter) error } -type RegisterDID200Response struct { +type ActivateServiceForDID200Response struct { } -func (response RegisterDID200Response) VisitRegisterDIDResponse(w http.ResponseWriter) error { +func (response ActivateServiceForDID200Response) VisitActivateServiceForDIDResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type RegisterDID202JSONResponse struct { +type ActivateServiceForDID202JSONResponse struct { // Reason Description of why registration failed. Reason string `json:"reason"` } -func (response RegisterDID202JSONResponse) VisitRegisterDIDResponse(w http.ResponseWriter) error { +func (response ActivateServiceForDID202JSONResponse) VisitActivateServiceForDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(202) return json.NewEncoder(w).Encode(response) } -type RegisterDID400ApplicationProblemPlusJSONResponse struct { +type ActivateServiceForDID400ApplicationProblemPlusJSONResponse struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1439,14 +1439,14 @@ type RegisterDID400ApplicationProblemPlusJSONResponse struct { Title string `json:"title"` } -func (response RegisterDID400ApplicationProblemPlusJSONResponse) VisitRegisterDIDResponse(w http.ResponseWriter) error { +func (response ActivateServiceForDID400ApplicationProblemPlusJSONResponse) VisitActivateServiceForDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type RegisterDIDdefaultApplicationProblemPlusJSONResponse struct { +type ActivateServiceForDIDdefaultApplicationProblemPlusJSONResponse struct { Body struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -1460,7 +1460,7 @@ type RegisterDIDdefaultApplicationProblemPlusJSONResponse struct { StatusCode int } -func (response RegisterDIDdefaultApplicationProblemPlusJSONResponse) VisitRegisterDIDResponse(w http.ResponseWriter) error { +func (response ActivateServiceForDIDdefaultApplicationProblemPlusJSONResponse) VisitActivateServiceForDIDResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(response.StatusCode) @@ -1469,21 +1469,21 @@ func (response RegisterDIDdefaultApplicationProblemPlusJSONResponse) VisitRegist // StrictServerInterface represents all server handlers. type StrictServerInterface interface { - // Retrieves the presentations of a discovery service. + // Retrieves the presentations of a Discovery Service. // (GET /discovery/{serviceID}) GetPresentations(ctx context.Context, request GetPresentationsRequestObject) (GetPresentationsResponseObject, error) - // Register a presentation on the discovery service. + // Register a presentation on the Discovery Service. // (POST /discovery/{serviceID}) RegisterPresentation(ctx context.Context, request RegisterPresentationRequestObject) (RegisterPresentationResponseObject, error) - // Searches for presentations registered on the discovery service. + // Searches for presentations registered on the Discovery Service. // (GET /discovery/{serviceID}/search) SearchPresentations(ctx context.Context, request SearchPresentationsRequestObject) (SearchPresentationsResponseObject, error) - // Client API to stop registering the given DID on the discovery service. + // Client API to unregister the given DID from the Discovery Service. // (DELETE /internal/discovery/v1/{serviceID}/{did}) - DeregisterDID(ctx context.Context, request DeregisterDIDRequestObject) (DeregisterDIDResponseObject, error) - // Client API to start registering the given DID on the discovery service. + DeactivateServiceForDID(ctx context.Context, request DeactivateServiceForDIDRequestObject) (DeactivateServiceForDIDResponseObject, error) + // Client API to activate a DID on the specified Discovery Service. // (POST /internal/discovery/v1/{serviceID}/{did}) - RegisterDID(ctx context.Context, request RegisterDIDRequestObject) (RegisterDIDResponseObject, error) + ActivateServiceForDID(ctx context.Context, request ActivateServiceForDIDRequestObject) (ActivateServiceForDIDResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -1581,52 +1581,52 @@ func (sh *strictHandler) SearchPresentations(ctx echo.Context, serviceID string, return nil } -// DeregisterDID operation middleware -func (sh *strictHandler) DeregisterDID(ctx echo.Context, serviceID string, did string) error { - var request DeregisterDIDRequestObject +// DeactivateServiceForDID operation middleware +func (sh *strictHandler) DeactivateServiceForDID(ctx echo.Context, serviceID string, did string) error { + var request DeactivateServiceForDIDRequestObject request.ServiceID = serviceID request.Did = did handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.DeregisterDID(ctx.Request().Context(), request.(DeregisterDIDRequestObject)) + return sh.ssi.DeactivateServiceForDID(ctx.Request().Context(), request.(DeactivateServiceForDIDRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "DeregisterDID") + handler = middleware(handler, "DeactivateServiceForDID") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(DeregisterDIDResponseObject); ok { - return validResponse.VisitDeregisterDIDResponse(ctx.Response()) + } else if validResponse, ok := response.(DeactivateServiceForDIDResponseObject); ok { + return validResponse.VisitDeactivateServiceForDIDResponse(ctx.Response()) } else if response != nil { return fmt.Errorf("unexpected response type: %T", response) } return nil } -// RegisterDID operation middleware -func (sh *strictHandler) RegisterDID(ctx echo.Context, serviceID string, did string) error { - var request RegisterDIDRequestObject +// ActivateServiceForDID operation middleware +func (sh *strictHandler) ActivateServiceForDID(ctx echo.Context, serviceID string, did string) error { + var request ActivateServiceForDIDRequestObject request.ServiceID = serviceID request.Did = did handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.RegisterDID(ctx.Request().Context(), request.(RegisterDIDRequestObject)) + return sh.ssi.ActivateServiceForDID(ctx.Request().Context(), request.(ActivateServiceForDIDRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "RegisterDID") + handler = middleware(handler, "ActivateServiceForDID") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(RegisterDIDResponseObject); ok { - return validResponse.VisitRegisterDIDResponse(ctx.Response()) + } else if validResponse, ok := response.(ActivateServiceForDIDResponseObject); ok { + return validResponse.VisitActivateServiceForDIDResponse(ctx.Response()) } else if response != nil { return fmt.Errorf("unexpected response type: %T", response) } diff --git a/discovery/client.go b/discovery/client.go index 7a52af7c92..7c0b3c8da2 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -34,18 +34,18 @@ import ( "time" ) -// registrationManager is responsible for managing registrations on a Discovery Service. +// clientRegistrationManager is a client component, responsible for managing registrations on a Discovery Service. // It automatically refreshes registered Verifiable Presentations when they are about to expire. -type registrationManager interface { - register(ctx context.Context, serviceID string, subjectDID did.DID) error - deregister(ctx context.Context, serviceID string, subjectDID did.DID) error - // refreshRegistrations is a blocking call to periodically refresh registrations. - // It checks for registrations to be refreshed at the specified interval. +type clientRegistrationManager interface { + activate(ctx context.Context, serviceID string, subjectDID did.DID) error + deactivate(ctx context.Context, serviceID string, subjectDID did.DID) error + // refresh is a blocking call to periodically refresh registrations. + // It checks for Verifiable Presentations that are about to expire, and should be refreshed on the Discovery Service. // It will exit when the given context is cancelled. - refreshVerifiablePresentations(ctx context.Context, interval time.Duration) + refresh(ctx context.Context, interval time.Duration) } -var _ registrationManager = &scheduledRegistrationManager{} +var _ clientRegistrationManager = &scheduledRegistrationManager{} type scheduledRegistrationManager struct { services map[string]ServiceDefinition @@ -64,31 +64,31 @@ func newRegistrationManager(services map[string]ServiceDefinition, store *sqlSto return instance } -func (r *scheduledRegistrationManager) register(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (r *scheduledRegistrationManager) activate(ctx context.Context, serviceID string, subjectDID did.DID) error { service, serviceExists := r.services[serviceID] if !serviceExists { return ErrServiceNotFound } // TODO: When to refresh? For now, we refresh when the registration is about to expire (75% of max age) refreshVPAfter := time.Now().Add(time.Duration(float64(service.PresentationMaxValidity)*0.75) * time.Second) - log.Logger().Debugf("Refreshing Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) - if err := r.store.updateDIDRegistrationTime(serviceID, subjectDID, &refreshVPAfter); err != nil { - return fmt.Errorf("unable to update DID registration: %w", err) + log.Logger().Debugf("Registering Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) + if err := r.store.updatePresentationRefreshTime(serviceID, subjectDID, &refreshVPAfter); err != nil { + return fmt.Errorf("unable to update DID registration time: %w", err) } err := r.registerPresentation(ctx, subjectDID, service) if err != nil { // retry registration asap var next time.Time - _ = r.store.updateDIDRegistrationTime(serviceID, subjectDID, &next) + _ = r.store.updatePresentationRefreshTime(serviceID, subjectDID, &next) return errors.Join(ErrPresentationRegistrationFailed, err) } log.Logger().Debugf("Successfully refreshed Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) return nil } -func (r *scheduledRegistrationManager) deregister(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (r *scheduledRegistrationManager) deactivate(ctx context.Context, serviceID string, subjectDID did.DID) error { // delete DID/service combination from DB, so it won't be registered again - err := r.store.updateDIDRegistrationTime(serviceID, subjectDID, nil) + err := r.store.updatePresentationRefreshTime(serviceID, subjectDID, nil) if err != nil { return err } @@ -161,19 +161,19 @@ func (r *scheduledRegistrationManager) buildPresentation(ctx context.Context, su func (r *scheduledRegistrationManager) doRefreshVerifiablePresentations(ctx context.Context, now time.Time) error { log.Logger().Debug("Refreshing Verifiable Presentations on Discovery Services") - serviceIDs, dids, err := r.store.getStaleDIDRegistrations(now) + serviceIDs, dids, err := r.store.getPresentationsToBeRefreshed(now) if err != nil { return err } for i, serviceID := range serviceIDs { - if err := r.register(ctx, serviceID, dids[i]); err != nil { + if err := r.activate(ctx, serviceID, dids[i]); err != nil { log.Logger().WithError(err).Warnf("Failed to refresh Verifiable Presentation (service=%s, did=%s)", serviceID, dids[i]) } } return nil } -func (r *scheduledRegistrationManager) refreshVerifiablePresentations(ctx context.Context, interval time.Duration) { +func (r *scheduledRegistrationManager) refresh(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() // do the first refresh immediately diff --git a/discovery/client_test.go b/discovery/client_test.go index a737d06ce5..cbbcf260c5 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -33,7 +33,7 @@ func Test_scheduledRegistrationManager_register(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.register(audit.TestContext(), testServiceID, aliceDID) + err := manager.activate(audit.TestContext(), testServiceID, aliceDID) require.NoError(t, err) }) @@ -49,7 +49,7 @@ func Test_scheduledRegistrationManager_register(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.register(audit.TestContext(), testServiceID, aliceDID) + err := manager.activate(audit.TestContext(), testServiceID, aliceDID) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "invoker error") @@ -64,7 +64,7 @@ func Test_scheduledRegistrationManager_register(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.register(audit.TestContext(), testServiceID, aliceDID) + err := manager.activate(audit.TestContext(), testServiceID, aliceDID) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "DID wallet does not have credentials required for registration on Discovery Service (service=usecase_v1, did=did:example:alice)") @@ -76,7 +76,7 @@ func Test_scheduledRegistrationManager_register(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.register(audit.TestContext(), "unknown", aliceDID) + err := manager.activate(audit.TestContext(), "unknown", aliceDID) require.EqualError(t, err, "discovery service not found") }) @@ -93,7 +93,7 @@ func Test_scheduledRegistrationManager_deregister(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.deregister(audit.TestContext(), testServiceID, aliceDID) + err := manager.deactivate(audit.TestContext(), testServiceID, aliceDID) assert.NoError(t, err) }) @@ -110,7 +110,7 @@ func Test_scheduledRegistrationManager_deregister(t *testing.T) { tag := Tag("taggy") require.NoError(t, store.add(testServiceID, vpAlice, &tag)) - err := manager.deregister(audit.TestContext(), testServiceID, aliceDID) + err := manager.deactivate(audit.TestContext(), testServiceID, aliceDID) assert.NoError(t, err) }) @@ -127,7 +127,7 @@ func Test_scheduledRegistrationManager_deregister(t *testing.T) { tag := Tag("taggy") require.NoError(t, store.add(testServiceID, vpAlice, &tag)) - err := manager.deregister(audit.TestContext(), testServiceID, aliceDID) + err := manager.deactivate(audit.TestContext(), testServiceID, aliceDID) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "remote error") @@ -162,11 +162,11 @@ func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) // Alice - _ = store.updateDIDRegistrationTime(testServiceID, aliceDID, &time.Time{}) + _ = store.updatePresentationRefreshTime(testServiceID, aliceDID, &time.Time{}) 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) // Bob - _ = store.updateDIDRegistrationTime(testServiceID, bobDID, &time.Time{}) + _ = store.updatePresentationRefreshTime(testServiceID, bobDID, &time.Time{}) 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) @@ -194,7 +194,7 @@ func Test_scheduledRegistrationManager_refreshRegistrations(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - manager.refreshVerifiablePresentations(ctx, time.Millisecond) + manager.refresh(ctx, time.Millisecond) }() // make sure the loop has at least once time.Sleep(5 * time.Millisecond) diff --git a/discovery/interface.go b/discovery/interface.go index 350bb3916c..bb81b3447e 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -90,9 +90,9 @@ var ErrPresentationRegistrationFailed = errors.New("registration of Verifiable P // Server defines the API for Discovery Servers. type Server interface { - // Add registers a presentation on the given Discovery Service. + // Register registers a presentation on the given Discovery Service. // If the presentation is not valid, or it does not conform to the Service ServiceDefinition, it returns an error. - Add(serviceID string, presentation vc.VerifiablePresentation) error + Register(serviceID string, presentation vc.VerifiablePresentation) error // Get retrieves the presentations for the given service, starting at the given timestamp. Get(serviceID string, startAt *Tag) ([]vc.VerifiablePresentation, *Tag, error) } @@ -103,16 +103,16 @@ type Client interface { // Query parameters are formatted as simple JSON paths, e.g. "issuer" or "credentialSubject.name". Search(serviceID string, query map[string]string) ([]SearchResult, error) - // Register causes a DID, in the form of a Verifiable Presentation, to be registered on a Discovery Service. + // ActivateServiceForDID causes a DID, in the form of a Verifiable Presentation, to be registered on a Discovery Service. // Registration will be attempted immediately, and automatically refreshed. // If the initial registration fails with ErrPresentationRegistrationFailed, registration will be retried. // If the function is called again for the same service/DID combination, it will try to refresh the registration. // It returns an error if the service or DID is invalid/unknown. - Register(ctx context.Context, serviceID string, subjectDID did.DID) error + ActivateServiceForDID(ctx context.Context, serviceID string, subjectDID did.DID) error - // Deregister removes the registration of a DID on a Discovery Service. + // DeactivateServiceForDID removes the registration of a DID on a Discovery Service. // It returns an error if the service or DID is invalid/unknown. - Deregister(ctx context.Context, serviceID string, subjectDID did.DID) error + DeactivateServiceForDID(ctx context.Context, serviceID string, subjectDID did.DID) error } // SearchResult is a single result of a search operation. diff --git a/discovery/mock.go b/discovery/mock.go index fc88be0e78..93b481fea8 100644 --- a/discovery/mock.go +++ b/discovery/mock.go @@ -41,20 +41,6 @@ func (m *MockServer) EXPECT() *MockServerMockRecorder { return m.recorder } -// Add mocks base method. -func (m *MockServer) Add(serviceID string, presentation vc.VerifiablePresentation) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Add", serviceID, presentation) - ret0, _ := ret[0].(error) - return ret0 -} - -// Add indicates an expected call of Add. -func (mr *MockServerMockRecorder) Add(serviceID, presentation any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockServer)(nil).Add), serviceID, presentation) -} - // Get mocks base method. func (m *MockServer) Get(serviceID string, startAt *Tag) ([]vc.VerifiablePresentation, *Tag, error) { m.ctrl.T.Helper() @@ -71,6 +57,20 @@ func (mr *MockServerMockRecorder) Get(serviceID, startAt any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockServer)(nil).Get), serviceID, startAt) } +// Register mocks base method. +func (m *MockServer) Register(serviceID string, presentation vc.VerifiablePresentation) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Register", serviceID, presentation) + ret0, _ := ret[0].(error) + return ret0 +} + +// Register indicates an expected call of Register. +func (mr *MockServerMockRecorder) Register(serviceID, presentation any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockServer)(nil).Register), serviceID, presentation) +} + // MockClient is a mock of Client interface. type MockClient struct { ctrl *gomock.Controller @@ -94,32 +94,32 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } -// Deregister mocks base method. -func (m *MockClient) Deregister(ctx context.Context, serviceID string, subjectDID did.DID) error { +// ActivateServiceForDID mocks base method. +func (m *MockClient) ActivateServiceForDID(ctx context.Context, serviceID string, subjectDID did.DID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Deregister", ctx, serviceID, subjectDID) + ret := m.ctrl.Call(m, "ActivateServiceForDID", ctx, serviceID, subjectDID) ret0, _ := ret[0].(error) return ret0 } -// Deregister indicates an expected call of Deregister. -func (mr *MockClientMockRecorder) Deregister(ctx, serviceID, subjectDID any) *gomock.Call { +// ActivateServiceForDID indicates an expected call of ActivateServiceForDID. +func (mr *MockClientMockRecorder) ActivateServiceForDID(ctx, serviceID, subjectDID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deregister", reflect.TypeOf((*MockClient)(nil).Deregister), ctx, serviceID, subjectDID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivateServiceForDID", reflect.TypeOf((*MockClient)(nil).ActivateServiceForDID), ctx, serviceID, subjectDID) } -// Register mocks base method. -func (m *MockClient) Register(ctx context.Context, serviceID string, subjectDID did.DID) error { +// DeactivateServiceForDID mocks base method. +func (m *MockClient) DeactivateServiceForDID(ctx context.Context, serviceID string, subjectDID did.DID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Register", ctx, serviceID, subjectDID) + ret := m.ctrl.Call(m, "DeactivateServiceForDID", ctx, serviceID, subjectDID) ret0, _ := ret[0].(error) return ret0 } -// Register indicates an expected call of Register. -func (mr *MockClientMockRecorder) Register(ctx, serviceID, subjectDID any) *gomock.Call { +// DeactivateServiceForDID indicates an expected call of DeactivateServiceForDID. +func (mr *MockClientMockRecorder) DeactivateServiceForDID(ctx, serviceID, subjectDID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), ctx, serviceID, subjectDID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeactivateServiceForDID", reflect.TypeOf((*MockClient)(nil).DeactivateServiceForDID), ctx, serviceID, subjectDID) } // Search mocks base method. diff --git a/discovery/module.go b/discovery/module.go index a2599fceff..a2679130d2 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -41,7 +41,7 @@ import ( const ModuleName = "Discovery" -// ErrServerModeDisabled is returned when a client invokes a Discovery Server (Add or Get) operation on the node, +// ErrServerModeDisabled is returned when a client invokes a Discovery Server (Register or Get) operation on the node, // for a Discovery Service which it doesn't serve. var ErrServerModeDisabled = errors.New("node is not a discovery server for this service") @@ -85,7 +85,7 @@ type Module struct { httpClient client.HTTPClient storageInstance storage.Engine store *sqlStore - registrationManager registrationManager + registrationManager clientRegistrationManager serverDefinitions map[string]ServiceDefinition allDefinitions map[string]ServiceDefinition vcrInstance vcr.VCR @@ -130,7 +130,7 @@ func (m *Module) Start() error { m.routines.Add(1) go func() { defer m.routines.Done() - m.registrationManager.refreshVerifiablePresentations(m.ctx, m.config.Client.RegistrationRefreshInterval) + m.registrationManager.refresh(m.ctx, m.config.Client.RegistrationRefreshInterval) }() return nil } @@ -149,9 +149,9 @@ func (m *Module) Config() interface{} { return &m.config } -// Add registers a presentation on the given Discovery Service. +// Register is a Discovery Server function that registers a presentation on the given Discovery Service. // See interface.go for more information. -func (m *Module) Add(serviceID string, presentation vc.VerifiablePresentation) error { +func (m *Module) Register(serviceID string, presentation vc.VerifiablePresentation) error { // First, simple sanity checks definition, isServer := m.serverDefinitions[serviceID] if !isServer { @@ -248,7 +248,7 @@ func (m *Module) validateRetraction(serviceID string, presentation vc.Verifiable return nil } -// Get retrieves the presentations for the given service, starting at the given tag. +// Get is a Discovery Server function that retrieves the presentations for the given service, starting at the given tag. // See interface.go for more information. func (m *Module) Get(serviceID string, tag *Tag) ([]vc.VerifiablePresentation, *Tag, error) { if _, exists := m.serverDefinitions[serviceID]; !exists { @@ -257,8 +257,10 @@ func (m *Module) Get(serviceID string, tag *Tag) ([]vc.VerifiablePresentation, * return m.store.get(serviceID, tag) } -func (m *Module) Register(ctx context.Context, serviceID string, subjectDID did.DID) error { - log.Logger().Debugf("Registering on Discovery Service (did=%s, service=%s)", subjectDID, serviceID) +// ActivateServiceForDID is a Discovery Client function that activates a service for a DID. +// See interface.go for more information. +func (m *Module) ActivateServiceForDID(ctx context.Context, serviceID string, subjectDID did.DID) error { + log.Logger().Debugf("Activating service for DID (did=%s, service=%s)", subjectDID, serviceID) isOwner, err := m.documentOwner.IsOwner(ctx, subjectDID) if err != nil { return err @@ -266,18 +268,20 @@ func (m *Module) Register(ctx context.Context, serviceID string, subjectDID did. if !isOwner { return errors.New("not owner of DID") } - err = m.registrationManager.register(ctx, serviceID, subjectDID) + err = m.registrationManager.activate(ctx, serviceID, subjectDID) if errors.Is(err, ErrPresentationRegistrationFailed) { - log.Logger().WithError(err).Warnf("Discovery Service registration failed, will be retried later (did=%s,service=%s)", subjectDID, serviceID) + log.Logger().WithError(err).Warnf("Presentation registration failed, will be retried later (did=%s,service=%s)", subjectDID, serviceID) } else if err == nil { - log.Logger().Infof("Successfully registered Discovery Service (did=%s,service=%s)", subjectDID, serviceID) + log.Logger().Infof("Successfully activated service for DID (did=%s,service=%s)", subjectDID, serviceID) } return err } -func (m *Module) Deregister(ctx context.Context, serviceID string, subjectDID did.DID) error { - log.Logger().Infof("Deregistering from Discovery Service (did=%s, service=%s)", subjectDID, serviceID) - return m.registrationManager.deregister(ctx, serviceID, subjectDID) +// DeactivateServiceForDID is a Discovery Client function that deactivates a service for a DID. +// See interface.go for more information. +func (m *Module) DeactivateServiceForDID(ctx context.Context, serviceID string, subjectDID did.DID) error { + log.Logger().Infof("Deactivating service for DID (did=%s, service=%s)", subjectDID, serviceID) + return m.registrationManager.deactivate(ctx, serviceID, subjectDID) } func loadDefinitions(directory string) (map[string]ServiceDefinition, error) { @@ -307,6 +311,8 @@ func loadDefinitions(directory string) (map[string]ServiceDefinition, error) { return result, nil } +// Search is a Discovery Client function that searches for presentations which credential(s) match the given query. +// See interface.go for more information. func (m *Module) Search(serviceID string, query map[string]string) ([]SearchResult, error) { service, exists := m.allDefinitions[serviceID] if !exists { diff --git a/discovery/module_test.go b/discovery/module_test.go index 849128c347..73f1133d94 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -46,21 +46,21 @@ func TestModule_Shutdown(t *testing.T) { require.NoError(t, module.Shutdown()) } -func Test_Module_Add(t *testing.T) { +func Test_Module_Register(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) t.Run("not a server", func(t *testing.T) { m, _, _ := setupModule(t, storageEngine) - err := m.Add("other", vpAlice) + err := m.Register("other", vpAlice) require.EqualError(t, err, "node is not a discovery server for this service") }) t.Run("VP verification fails (e.g. invalid signature)", func(t *testing.T) { m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("failed")) - err := m.Add(testServiceID, vpAlice) + err := m.Register(testServiceID, vpAlice) require.EqualError(t, err, "presentation is invalid for registration\npresentation verification failed: failed") _, tag, err := m.Get(testServiceID, nil) @@ -72,9 +72,9 @@ func Test_Module_Add(t *testing.T) { m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - err := m.Add(testServiceID, vpAlice) + err := m.Register(testServiceID, vpAlice) assert.NoError(t, err) - err = m.Add(testServiceID, vpAlice) + err = m.Register(testServiceID, vpAlice) assert.ErrorIs(t, err, ErrPresentationAlreadyExists) }) t.Run("valid for too long", func(t *testing.T) { @@ -84,12 +84,12 @@ func Test_Module_Add(t *testing.T) { m.allDefinitions[testServiceID] = def m.serverDefinitions[testServiceID] = def - err := m.Add(testServiceID, vpAlice) + err := m.Register(testServiceID, vpAlice) assert.EqualError(t, err, "presentation is invalid for registration\npresentation is valid for too long (max 1s)") }) t.Run("no expiration", func(t *testing.T) { m, _, _ := setupModule(t, storageEngine) - err := m.Add(testServiceID, createPresentationCustom(aliceDID, func(claims map[string]interface{}, _ *vc.VerifiablePresentation) { + err := m.Register(testServiceID, createPresentationCustom(aliceDID, func(claims map[string]interface{}, _ *vc.VerifiablePresentation) { claims[jwt.AudienceKey] = []string{testServiceID} delete(claims, "exp") })) @@ -102,12 +102,12 @@ func Test_Module_Add(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} delete(claims, "jti") }, vcAlice) - err := m.Add(testServiceID, vpWithoutID) + err := m.Register(testServiceID, vpWithoutID) assert.ErrorIs(t, err, errPresentationWithoutID) }) t.Run("not a JWT", func(t *testing.T) { m, _, _ := setupModule(t, storageEngine) - err := m.Add(testServiceID, vc.VerifiablePresentation{}) + err := m.Register(testServiceID, vc.VerifiablePresentation{}) assert.ErrorIs(t, err, errUnsupportedPresentationFormat) }) @@ -116,7 +116,7 @@ func Test_Module_Add(t *testing.T) { m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil) - err := m.Add(testServiceID, vpAlice) + err := m.Register(testServiceID, vpAlice) require.NoError(t, err) _, tag, err := m.Get(testServiceID, nil) @@ -133,7 +133,7 @@ func Test_Module_Add(t *testing.T) { vpAlice := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { claims[jwt.AudienceKey] = []string{testServiceID} }, vcAlice) - err := m.Add(testServiceID, vpAlice) + err := m.Register(testServiceID, vpAlice) assert.ErrorIs(t, err, errPresentationValidityExceedsCredentials) }) t.Run("not conform to Presentation Definition", func(t *testing.T) { @@ -143,7 +143,7 @@ func Test_Module_Add(t *testing.T) { otherVP := createPresentationCustom(unsupportedDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { claims[jwt.AudienceKey] = []string{testServiceID} }, createCredential(unsupportedDID, unsupportedDID, nil, nil)) - err := m.Add(testServiceID, otherVP) + err := m.Register(testServiceID, otherVP) require.ErrorContains(t, err, "presentation does not fulfill Presentation ServiceDefinition") _, tag, _ := m.Get(testServiceID, nil) @@ -160,14 +160,14 @@ func Test_Module_Add(t *testing.T) { m, presentationVerifier, _ := setupModule(t, storageEngine) presentationVerifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).Times(2) - err := m.Add(testServiceID, vpAlice) + err := m.Register(testServiceID, vpAlice) require.NoError(t, err) - err = m.Add(testServiceID, vpAliceRetract) + err = m.Register(testServiceID, vpAliceRetract) assert.NoError(t, err) }) t.Run("non-existent presentation", func(t *testing.T) { m, _, _ := setupModule(t, storageEngine) - err := m.Add(testServiceID, vpAliceRetract) + err := m.Register(testServiceID, vpAliceRetract) assert.ErrorIs(t, err, errRetractionReferencesUnknownPresentation) }) t.Run("must not contain credentials", func(t *testing.T) { @@ -176,7 +176,7 @@ func Test_Module_Add(t *testing.T) { vp.Type = append(vp.Type, retractionPresentationType) claims[jwt.AudienceKey] = []string{testServiceID} }, vcAlice) - err := m.Add(testServiceID, vp) + err := m.Register(testServiceID, vp) assert.ErrorIs(t, err, errRetractionContainsCredentials) }) t.Run("missing 'retract_jti' claim", func(t *testing.T) { @@ -185,7 +185,7 @@ func Test_Module_Add(t *testing.T) { vp.Type = append(vp.Type, retractionPresentationType) claims[jwt.AudienceKey] = []string{testServiceID} }) - err := m.Add(testServiceID, vp) + err := m.Register(testServiceID, vp) assert.ErrorIs(t, err, errInvalidRetractionJTIClaim) }) t.Run("'retract_jti' claim is not a string", func(t *testing.T) { @@ -195,7 +195,7 @@ func Test_Module_Add(t *testing.T) { claims["retract_jti"] = 10 claims[jwt.AudienceKey] = []string{testServiceID} }) - err := m.Add(testServiceID, vp) + err := m.Register(testServiceID, vp) assert.ErrorIs(t, err, errInvalidRetractionJTIClaim) }) t.Run("'retract_jti' claim is an empty string", func(t *testing.T) { @@ -205,7 +205,7 @@ func Test_Module_Add(t *testing.T) { claims["retract_jti"] = "" claims[jwt.AudienceKey] = []string{testServiceID} }) - err := m.Add(testServiceID, vp) + err := m.Register(testServiceID, vp) assert.ErrorIs(t, err, errInvalidRetractionJTIClaim) }) }) @@ -320,14 +320,14 @@ func TestModule_Search(t *testing.T) { }) } -func TestModule_Register(t *testing.T) { +func TestModule_ActivateServiceForDID(t *testing.T) { t.Run("not owned", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) m, _, documentOwner := setupModule(t, storageEngine) documentOwner.EXPECT().IsOwner(gomock.Any(), aliceDID).Return(false, nil) - err := m.Register(context.Background(), testServiceID, aliceDID) + err := m.ActivateServiceForDID(context.Background(), testServiceID, aliceDID) require.EqualError(t, err, "not owner of DID") }) @@ -340,7 +340,7 @@ func TestModule_Register(t *testing.T) { wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed")).MinTimes(1) documentOwner.EXPECT().IsOwner(gomock.Any(), aliceDID).Return(true, nil) - err := m.Register(context.Background(), testServiceID, aliceDID) + err := m.ActivateServiceForDID(context.Background(), testServiceID, aliceDID) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) }) diff --git a/discovery/store.go b/discovery/store.go index c0d5e0cdb6..34494d9b10 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -104,19 +104,19 @@ func (l credentialPropertyRecord) TableName() string { return "discovery_credential_prop" } -// didRegistrationRecord is a tab-keeping record for clients to keep track of which DIDs should be registered on which Discovery Services. -type didRegistrationRecord struct { +// presentationRefreshRecord is a tab-keeping record for clients to keep track of which DIDs should be registered on which Discovery Services. +type presentationRefreshRecord struct { // ServiceID refers to the entry record in discovery_service ServiceID string `gorm:"primaryKey"` // Did is Did that should be registered on the service. Did string `gorm:"primaryKey"` - // NextRegistration is the timestamp (seconds since Unix epoch) when the registration on the Discovery Service should be refreshed. - NextRegistration int64 + // NextRefresh is the timestamp (seconds since Unix epoch) when the registration on the Discovery Service should be refreshed. + NextRefresh int64 } // TableName returns the table name for this DTO. -func (l didRegistrationRecord) TableName() string { - return "discovery_did_registration" +func (l presentationRefreshRecord) TableName() string { + return "discovery_presentation_refresh" } type sqlStore struct { @@ -418,24 +418,24 @@ func (s *sqlStore) removeExpired() (int, error) { return int(result.RowsAffected), nil } -// updateDIDRegistrationTime creates/updates the next registration time for a DID on a Discovery Service. +// 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) updateDIDRegistrationTime(serviceID string, subjectDID did.DID, nextRegistration *time.Time) error { +func (s *sqlStore) updatePresentationRefreshTime(serviceID string, subjectDID did.DID, nextRefresh *time.Time) error { return s.db.Transaction(func(tx *gorm.DB) error { - if nextRegistration == nil { + if nextRefresh == nil { // Delete registration - return tx.Delete(&didRegistrationRecord{}, "service_id = ? AND did = ?", serviceID, subjectDID.String()).Error + return tx.Delete(&presentationRefreshRecord{}, "service_id = ? AND did = ?", serviceID, subjectDID.String()).Error } // Create or update it - return tx.Save(didRegistrationRecord{Did: subjectDID.String(), ServiceID: serviceID, NextRegistration: nextRegistration.Unix()}).Error + return tx.Save(presentationRefreshRecord{Did: subjectDID.String(), ServiceID: serviceID, NextRefresh: nextRefresh.Unix()}).Error }) } -// getStaleDIDRegistrations returns all DID discovery service registrations that are due for refreshing. +// getPresentationsToBeRefreshed returns all DID discovery service registrations that are due for refreshing. // It returns a slice of service IDs and associated DIDs. -func (s *sqlStore) getStaleDIDRegistrations(now time.Time) ([]string, []did.DID, error) { - var rows []didRegistrationRecord - if err := s.db.Find(&rows, "next_registration < ?", now.Unix()).Error; err != nil { +func (s *sqlStore) getPresentationsToBeRefreshed(now time.Time) ([]string, []did.DID, error) { + var rows []presentationRefreshRecord + if err := s.db.Find(&rows, "next_refresh < ?", now.Unix()).Error; err != nil { return nil, nil, err } var dids []did.DID @@ -443,7 +443,7 @@ func (s *sqlStore) getStaleDIDRegistrations(now time.Time) ([]string, []did.DID, for _, row := range rows { parsedDID, err := did.ParseDID(row.Did) if err != nil { - log.Logger().WithError(err).Errorf("Invalid DID in discovery DID registration: %s", row.Did) + log.Logger().WithError(err).Errorf("Invalid DID in discovery presentation refresh table: %s", row.Did) continue } dids = append(dids, *parsedDID) diff --git a/discovery/store_test.go b/discovery/store_test.go index 8f35d653f7..6f24ecbc9a 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -367,40 +367,40 @@ func Test_sqlStore_getStaleDIDRegistrations(t *testing.T) { now := time.Now() t.Run("empty list", func(t *testing.T) { c := setupStore(t, storageEngine.GetSQLDatabase()) - serviceIDs, dids, err := c.getStaleDIDRegistrations(now) + serviceIDs, dids, err := c.getPresentationsToBeRefreshed(now) require.NoError(t, err) assert.Empty(t, serviceIDs) assert.Empty(t, dids) }) t.Run("1 entry, not stale", func(t *testing.T) { c := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, &now)) - serviceIDs, dids, err := c.getStaleDIDRegistrations(time.Now().Add(-1 * time.Hour)) + require.NoError(t, c.updatePresentationRefreshTime(testServiceID, aliceDID, &now)) + serviceIDs, dids, err := c.getPresentationsToBeRefreshed(time.Now().Add(-1 * time.Hour)) require.NoError(t, err) assert.Empty(t, serviceIDs) assert.Empty(t, dids) }) t.Run("1 entry, stale", func(t *testing.T) { c := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, &now)) - serviceIDs, dids, err := c.getStaleDIDRegistrations(time.Now().Add(time.Hour)) + require.NoError(t, c.updatePresentationRefreshTime(testServiceID, aliceDID, &now)) + serviceIDs, dids, err := c.getPresentationsToBeRefreshed(time.Now().Add(time.Hour)) require.NoError(t, err) assert.Equal(t, []string{testServiceID}, serviceIDs) assert.Equal(t, []did.DID{aliceDID}, dids) }) t.Run("does not return removed entry", func(t *testing.T) { c := setupStore(t, storageEngine.GetSQLDatabase()) - require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, &now)) + require.NoError(t, c.updatePresentationRefreshTime(testServiceID, aliceDID, &now)) // Assert it's there - serviceIDs, dids, err := c.getStaleDIDRegistrations(time.Now().Add(time.Hour)) + serviceIDs, dids, err := c.getPresentationsToBeRefreshed(time.Now().Add(time.Hour)) require.NoError(t, err) assert.Equal(t, []string{testServiceID}, serviceIDs) assert.Equal(t, []did.DID{aliceDID}, dids) // Remove it - require.NoError(t, c.updateDIDRegistrationTime(testServiceID, aliceDID, nil)) - serviceIDs, dids, err = c.getStaleDIDRegistrations(time.Now().Add(time.Hour)) + require.NoError(t, c.updatePresentationRefreshTime(testServiceID, aliceDID, nil)) + serviceIDs, dids, err = c.getPresentationsToBeRefreshed(time.Now().Add(time.Hour)) require.NoError(t, err) assert.Empty(t, serviceIDs) assert.Empty(t, dids) diff --git a/docs/_static/discovery/v1.yaml b/docs/_static/discovery/v1.yaml index 0481efb955..f297d6e40f 100644 --- a/docs/_static/discovery/v1.yaml +++ b/docs/_static/discovery/v1.yaml @@ -140,7 +140,7 @@ paths: schema: type: string post: - summary: Client API to register the given DID on the Discovery Service. + summary: Client API to activate a DID on the specified Discovery Service. description: | An API provided by the discovery client that will cause the given DID to be registered on the specified Discovery Service. Registration of a Verifiable Presentation will be attempted immediately, and it will be automatically refreshed. @@ -152,14 +152,14 @@ paths: error returns: * 400 - incorrect input: invalid/unknown service or DID. - operationId: registerDID + operationId: activateServiceForDID tags: - discovery responses: "200": - description: Registration at Discovery Service was successful. + description: Activation was successful. "202": - description: Registration at Discovery Service failed, but will be re-attempted later. + description: Activation was successful, but registration of the Verifiable Presentation failed (but will be automatically re-attempted later). content: application/json: schema: @@ -182,17 +182,17 @@ paths: error returns: * 400 - incorrect input: invalid/unknown service or DID. - operationId: unregisterDID + operationId: deactivateServiceForDID tags: - discovery responses: "200": description: | - DID was successfully unregistered from the Discovery Service. - Active Verifiable Presentation was removed from the remote Discovery Server (if applicable). + DID was successfully deactivated from the Discovery Service. + The active Verifiable Presentation was removed from the remote Discovery Server (if applicable). "202": description: | - DID was successfully unregistered from the Discovery Service, but failed to remove the active Verifiable Presentation registration from the remote Discovery Server. + DID was successfully deactivated from the Discovery Service, but failed to remove the active Verifiable Presentation registration from the remote Discovery Server. Applications might want to retry this API call later, or simply let the presentation expire. content: application/json: diff --git a/storage/sql_migrations/003_discoveryservice_client_registration.sql b/storage/sql_migrations/003_discoveryservice_client_registration.sql index 46a9baadf1..2dcc55eea2 100644 --- a/storage/sql_migrations/003_discoveryservice_client_registration.sql +++ b/storage/sql_migrations/003_discoveryservice_client_registration.sql @@ -1,20 +1,20 @@ -- migrate:up -- discovery_did_registration contains the DIDs that should be registered on the specified Discovery Service(s). -create table discovery_did_registration +create table discovery_presentation_refresh ( -- service_id is the ID of the Discover Service that the DID should be registered on. -- It comes from the service definition. - service_id varchar(200) not null, + service_id varchar(200) not null, -- did is the DID that should be registered on the Discovery Service. - did varchar(500) not null, - -- next_registration is the timestamp (seconds since Unix epoch) when the registration on the + did varchar(500) not null, + -- next_refresh is the timestamp (seconds since Unix epoch) when the registration on the -- Discovery Service should be refreshed. - next_registration integer not null, + next_refresh integer not null, primary key (service_id, did), - constraint fk_discovery_did_registration_service foreign key (service_id) references discovery_service (id) on delete cascade + constraint fk_discovery_presentation_refresh_service foreign key (service_id) references discovery_service (id) on delete cascade ); -- index for the next_registration column, used when checking which registrations need to be refreshed -create index idx_discovery_did_registration_refresh on discovery_did_registration (next_registration); +create index idx_discovery_presentation_refresh on discovery_presentation_refresh (next_refresh); -- migrate:down -drop table discovery_did_registration; +drop table discovery_presentation_refresh; From 9482db5fc5fe6167c6e811c0a4f88cc346422e75 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 12 Jan 2024 10:23:25 +0100 Subject: [PATCH 11/15] leftover rename --- discovery/client.go | 24 ++++++++++++------------ discovery/client_test.go | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/discovery/client.go b/discovery/client.go index 7c0b3c8da2..657e71ab0f 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -45,17 +45,17 @@ type clientRegistrationManager interface { refresh(ctx context.Context, interval time.Duration) } -var _ clientRegistrationManager = &scheduledRegistrationManager{} +var _ clientRegistrationManager = &defaultClientRegistrationManager{} -type scheduledRegistrationManager struct { +type defaultClientRegistrationManager struct { services map[string]ServiceDefinition store *sqlStore client client.HTTPClient vcr vcr.VCR } -func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR) *scheduledRegistrationManager { - instance := &scheduledRegistrationManager{ +func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR) *defaultClientRegistrationManager { + instance := &defaultClientRegistrationManager{ services: services, store: store, client: client, @@ -64,7 +64,7 @@ func newRegistrationManager(services map[string]ServiceDefinition, store *sqlSto return instance } -func (r *scheduledRegistrationManager) activate(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (r *defaultClientRegistrationManager) activate(ctx context.Context, serviceID string, subjectDID did.DID) error { service, serviceExists := r.services[serviceID] if !serviceExists { return ErrServiceNotFound @@ -86,7 +86,7 @@ func (r *scheduledRegistrationManager) activate(ctx context.Context, serviceID s return nil } -func (r *scheduledRegistrationManager) deactivate(ctx context.Context, serviceID string, subjectDID did.DID) error { +func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, serviceID string, subjectDID did.DID) error { // delete DID/service combination from DB, so it won't be registered again err := r.store.updatePresentationRefreshTime(serviceID, subjectDID, nil) if err != nil { @@ -119,7 +119,7 @@ func (r *scheduledRegistrationManager) deactivate(ctx context.Context, serviceID return nil } -func (r *scheduledRegistrationManager) registerPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) error { +func (r *defaultClientRegistrationManager) registerPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) error { presentation, err := r.findCredentialsAndBuildPresentation(ctx, subjectDID, service) if err != nil { return err @@ -127,7 +127,7 @@ func (r *scheduledRegistrationManager) registerPresentation(ctx context.Context, return r.client.Register(ctx, service.Endpoint, *presentation) } -func (r *scheduledRegistrationManager) findCredentialsAndBuildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) (*vc.VerifiablePresentation, error) { +func (r *defaultClientRegistrationManager) findCredentialsAndBuildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) (*vc.VerifiablePresentation, error) { credentials, err := r.vcr.Wallet().List(ctx, subjectDID) if err != nil { return nil, err @@ -142,7 +142,7 @@ func (r *scheduledRegistrationManager) findCredentialsAndBuildPresentation(ctx c return r.buildPresentation(ctx, subjectDID, service, matchingCredentials, nil) } -func (r *scheduledRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, +func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, credentials []vc.VerifiableCredential, additionalProperties map[string]interface{}) (*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. @@ -159,7 +159,7 @@ func (r *scheduledRegistrationManager) buildPresentation(ctx context.Context, su }, &subjectDID, false) } -func (r *scheduledRegistrationManager) doRefreshVerifiablePresentations(ctx context.Context, now time.Time) error { +func (r *defaultClientRegistrationManager) doRefresh(ctx context.Context, now time.Time) error { log.Logger().Debug("Refreshing Verifiable Presentations on Discovery Services") serviceIDs, dids, err := r.store.getPresentationsToBeRefreshed(now) if err != nil { @@ -173,12 +173,12 @@ func (r *scheduledRegistrationManager) doRefreshVerifiablePresentations(ctx cont return nil } -func (r *scheduledRegistrationManager) refresh(ctx context.Context, interval time.Duration) { +func (r *defaultClientRegistrationManager) refresh(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() // do the first refresh immediately do := func() { - if err := r.doRefreshVerifiablePresentations(audit.Context(ctx, "app", ModuleName, "RefreshVerifiablePresentations"), time.Now()); err != nil { + if err := r.doRefresh(audit.Context(ctx, "app", ModuleName, "RefreshVerifiablePresentations"), time.Now()); err != nil { log.Logger().WithError(err).Errorf("Failed to refresh Verifiable Presentations") } } diff --git a/discovery/client_test.go b/discovery/client_test.go index cbbcf260c5..2bafc8fb1b 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -145,7 +145,7 @@ func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.doRefreshVerifiablePresentations(audit.TestContext(), time.Now()) + err := manager.doRefresh(audit.TestContext(), time.Now()) require.NoError(t, err) }) @@ -170,7 +170,7 @@ func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { 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) - err := manager.doRefreshVerifiablePresentations(audit.TestContext(), time.Now()) + err := manager.doRefresh(audit.TestContext(), time.Now()) require.NoError(t, err) }) From 5786a63656284bd5b718847cd31127b57edc44a5 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 12 Jan 2024 10:50:13 +0100 Subject: [PATCH 12/15] copyright --- discovery/client_test.go | 18 ++++++++++++++++++ discovery/config_test.go | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/discovery/client_test.go b/discovery/client_test.go index 2bafc8fb1b..fd232d17f5 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -1,3 +1,21 @@ +/* + * 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 discovery import ( diff --git a/discovery/config_test.go b/discovery/config_test.go index 689d7816d4..643f485c4a 100644 --- a/discovery/config_test.go +++ b/discovery/config_test.go @@ -1,3 +1,21 @@ +/* + * 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 discovery import ( From 4af66f030d20f1e466b4be6fd83cd71b6362b872 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Fri, 12 Jan 2024 11:35:35 +0100 Subject: [PATCH 13/15] Update docs/_static/discovery/v1.yaml Co-authored-by: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> --- docs/_static/discovery/v1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/discovery/v1.yaml b/docs/_static/discovery/v1.yaml index f297d6e40f..b1593be79e 100644 --- a/docs/_static/discovery/v1.yaml +++ b/docs/_static/discovery/v1.yaml @@ -192,7 +192,7 @@ paths: The active Verifiable Presentation was removed from the remote Discovery Server (if applicable). "202": description: | - DID was successfully deactivated from the Discovery Service, but failed to remove the active Verifiable Presentation registration from the remote Discovery Server. + DID was successfully deactivated from the Discovery Service, but failed to remove the active Verifiable Presentation registration from the remote Discovery Server. The registration will be removed by the Discovery Server when the active Verifiable Presentation expires. Applications might want to retry this API call later, or simply let the presentation expire. content: application/json: From 0ce9e08d4a16037d39e56cfe1d73699ef034f6cd Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 12 Jan 2024 12:53:35 +0100 Subject: [PATCH 14/15] Feedback --- discovery/client.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/discovery/client.go b/discovery/client.go index 657e71ab0f..bb6d0bb7e0 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -69,20 +69,24 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service if !serviceExists { return ErrServiceNotFound } - // TODO: When to refresh? For now, we refresh when the registration is about to expire (75% of max age) - refreshVPAfter := time.Now().Add(time.Duration(float64(service.PresentationMaxValidity)*0.75) * time.Second) - log.Logger().Debugf("Registering Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) - if err := r.store.updatePresentationRefreshTime(serviceID, subjectDID, &refreshVPAfter); err != nil { - return fmt.Errorf("unable to update DID registration time: %w", err) + var asSoonAsPossible time.Time + if err := r.store.updatePresentationRefreshTime(serviceID, subjectDID, &asSoonAsPossible); err != nil { + return err } + log.Logger().Debugf("Registering Verifiable Presentation on Discovery Service (service=%s, did=%s)", service.ID, subjectDID) err := r.registerPresentation(ctx, subjectDID, service) if err != nil { - // retry registration asap - var next time.Time - _ = r.store.updatePresentationRefreshTime(serviceID, subjectDID, &next) + // failed, will be retried on next scheduled refresh return errors.Join(ErrPresentationRegistrationFailed, err) } - log.Logger().Debugf("Successfully refreshed Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) + log.Logger().Debugf("Successfully registered Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) + + // Set presentation to be refreshed before it expires + // TODO: When to refresh? For now, we refresh when the registration is about to expire (75% of max age) + refreshVPAfter := time.Now().Add(time.Duration(float64(service.PresentationMaxValidity)*0.75) * time.Second) + if err := r.store.updatePresentationRefreshTime(serviceID, subjectDID, &refreshVPAfter); err != nil { + return fmt.Errorf("unable to update Verifiable Presentation refresh time: %w", err) + } return nil } From 75ad0b38c02ad15dfcca4dcaa88a68a28a1b20ee Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 16 Jan 2024 14:43:47 +0100 Subject: [PATCH 15/15] Use client timeout --- discovery/module.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery/module.go b/discovery/module.go index a2679130d2..522c91246c 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -116,7 +116,7 @@ func (m *Module) Configure(serverConfig core.ServerConfig) error { } m.serverDefinitions = serverDefinitions } - m.httpClient = client.New(serverConfig.Strictmode, 10*time.Second, nil) + m.httpClient = client.New(serverConfig.Strictmode, serverConfig.HTTPClient.Timeout, nil) return nil }