diff --git a/network/api/v1/api.go b/network/api/v1/api.go index 5127066a5..8089f877c 100644 --- a/network/api/v1/api.go +++ b/network/api/v1/api.go @@ -25,6 +25,7 @@ import ( "github.com/labstack/echo/v4" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/network/log" + "net/http" "time" "github.com/nuts-foundation/nuts-node/core" @@ -42,10 +43,19 @@ type Wrapper struct { func (a *Wrapper) Routes(router core.EchoRouter) { RegisterHandlers(router, NewStrictHandler(a, []StrictMiddlewareFunc{ + func(f StrictHandlerFunc, operationID string) StrictHandlerFunc { // fast fail if did:nuts is disabled + return func(ctx echo.Context, args interface{}) (interface{}, error) { + if a.Service.Disabled() { + return nil, network.ErrDIDNutsDisabled + } + return f(ctx, args) + } + }, func(f StrictHandlerFunc, operationID string) StrictHandlerFunc { return func(ctx echo.Context, request interface{}) (response interface{}, err error) { ctx.Set(core.OperationIDContextKey, operationID) ctx.Set(core.ModuleNameContextKey, network.ModuleName) + ctx.Set(core.StatusCodeResolverContextKey, a) return f(ctx, request) } }, @@ -55,6 +65,13 @@ func (a *Wrapper) Routes(router core.EchoRouter) { })) } +// ResolveStatusCode maps errors returned by this API to specific HTTP status codes. +func (a *Wrapper) ResolveStatusCode(err error) int { + return core.ResolveStatusCode(err, map[error]int{ + network.ErrDIDNutsDisabled: http.StatusBadRequest, + }) +} + // ListTransactions lists all transactions func (a *Wrapper) ListTransactions(_ context.Context, request ListTransactionsRequestObject) (ListTransactionsResponseObject, error) { // Parse the start/end params, which have default values diff --git a/network/interface.go b/network/interface.go index ee49c2309..aef5ed4cf 100644 --- a/network/interface.go +++ b/network/interface.go @@ -61,14 +61,13 @@ type Transactions interface { DiscoverServices(updatedDID did.DID) // AddressBook returns the list of contacts in the address book. AddressBook() []transport.Contact + // Disabled returns true if core.ServerConfig.DIDMethods does not contain 'nuts' + Disabled() bool } // EventType defines a type for specifying the kind of events that can be published/subscribed on the Network. type EventType string -// AnyPayloadType is a wildcard that matches with any payload type. -const AnyPayloadType = "*" - // Receiver defines a callback function for processing transactions/payloads received by the DAG. type Receiver func(transaction dag.Transaction, payload []byte) error diff --git a/network/mock.go b/network/mock.go index f4ae30bfe..560aea9ae 100644 --- a/network/mock.go +++ b/network/mock.go @@ -86,6 +86,20 @@ func (mr *MockTransactionsMockRecorder) CreateTransaction(ctx, spec any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransaction", reflect.TypeOf((*MockTransactions)(nil).CreateTransaction), ctx, spec) } +// Disabled mocks base method. +func (m *MockTransactions) Disabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Disabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Disabled indicates an expected call of Disabled. +func (mr *MockTransactionsMockRecorder) Disabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disabled", reflect.TypeOf((*MockTransactions)(nil).Disabled)) +} + // DiscoverServices mocks base method. func (m *MockTransactions) DiscoverServices(updatedDID did.DID) { m.ctrl.T.Helper() diff --git a/network/network.go b/network/network.go index b573a7134..0f148d2fe 100644 --- a/network/network.go +++ b/network/network.go @@ -26,6 +26,7 @@ import ( "errors" "fmt" "net" + "slices" "strings" "sync/atomic" "time" @@ -66,11 +67,15 @@ const ( newNodeConnectionDelay = 5 * time.Minute ) +// ErrDIDNutsDisabled is returned from certain API methods when the core.ServerConfig.DIDMethods does not contain "nuts" +var ErrDIDNutsDisabled = errors.New("network operations not supported; did:nuts support not configured") + // defaultBBoltOptions are given to bbolt, allows for package local adjustments during test var defaultBBoltOptions = bbolt.DefaultOptions // Network implements Transactions interface and Engine functions. type Network struct { + disabled bool // node is running without did:nuts support config Config certificate tls.Certificate trustStore *core.TrustStore @@ -96,6 +101,9 @@ type Network struct { // CheckHealth performs health checks for the network engine. func (n *Network) CheckHealth() map[string]core.Health { + if n.disabled { + return nil + } results := make(map[string]core.Health) if n.certificate.Leaf != nil { results[healthTLS] = n.checkNodeTLSHealth() @@ -134,6 +142,9 @@ func (n *Network) checkNodeTLSHealth() core.Health { } func (n *Network) Migrate() error { + if n.disabled { + return nil + } return n.state.Migrate() } @@ -167,6 +178,10 @@ func NewNetworkInstance( // Configure configures the Network subsystem func (n *Network) Configure(config core.ServerConfig) error { + if !slices.Contains(config.DIDMethods, "nuts") { + n.disabled = true + return nil + } var err error dagStore, err := n.storeProvider.GetKVStore("data", storage.PersistentStorageClass) if err != nil { @@ -269,11 +284,7 @@ func (n *Network) Configure(config core.ServerConfig) error { } else { // Not allowed in strict mode for security reasons: only intended for demo/workshop purposes. if config.Strictmode { - if len(n.config.BootstrapNodes) == 0 && n.assumeNewNode { - log.Logger().Info("It appears the gRPC network will not be used (no bootstrap nodes and an empty network state), so disabled TLS is accepted even with strict mode enabled.") - } else { - return errors.New("disabling TLS in strict mode is not allowed") - } + return errors.New("disabling TLS in strict mode is not allowed") } authenticator = grpc.NewDummyAuthenticator(nil) } @@ -313,7 +324,7 @@ func (n *Network) Configure(config core.ServerConfig) error { } func (n *Network) DiscoverServices(updatedDID did.DID) { - if !n.config.EnableDiscovery { + if n.disabled || !n.config.EnableDiscovery { return } document, _, err := n.didStore.Resolve(updatedDID, nil) @@ -363,6 +374,9 @@ func (n *Network) Config() interface{} { // Start initiates the Network subsystem func (n *Network) Start() error { + if n.disabled { + return nil + } startTime := time.Now() n.startTime.Store(&startTime) @@ -571,6 +585,9 @@ func (n *Network) selfTestNutsCommAddress(nutsComm transport.NutsCommURL) error // The receiver is called when a transaction is added to the DAG. // It's only called if the given dag.NotificationFilter's match. func (n *Network) Subscribe(name string, subscriber dag.ReceiverFn, options ...SubscriberOption) error { + if n.disabled { + return nil + } notifierOptions := make([]dag.NotifierOption, len(options)) for i, o := range options { notifierOptions[i] = o() @@ -609,12 +626,18 @@ func (n *Network) CleanupSubscriberEvents(subscriberName, errorPrefix string) er // GetTransaction retrieves the transaction for the given reference. If the transaction is not known, an error is returned. func (n *Network) GetTransaction(transactionRef hash.SHA256Hash) (dag.Transaction, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } return n.state.GetTransaction(context.Background(), transactionRef) } // GetTransactionPayload retrieves the transaction Payload for the given transaction. If the transaction or Payload is not found // nil is returned. func (n *Network) GetTransactionPayload(transactionRef hash.SHA256Hash) ([]byte, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } transaction, err := n.state.GetTransaction(context.Background(), transactionRef) if err != nil { if errors.Is(err, dag.ErrTransactionNotFound) { @@ -628,11 +651,17 @@ func (n *Network) GetTransactionPayload(transactionRef hash.SHA256Hash) ([]byte, // ListTransactionsInRange returns all transactions known to this Network instance with lamport clock value between startInclusive and endExclusive. func (n *Network) ListTransactionsInRange(startInclusive uint32, endExclusive uint32) ([]dag.Transaction, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } return n.state.FindBetweenLC(context.Background(), startInclusive, endExclusive) } // CreateTransaction creates a new transaction from the given template. func (n *Network) CreateTransaction(ctx context.Context, template Template) (dag.Transaction, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } payloadHash := hash.SHA256Sum(template.Payload) log.Logger(). WithField(core.LogFieldTransactionType, template.Type). @@ -743,6 +772,9 @@ func (n *Network) calculateLamportClock(ctx context.Context, prevs []hash.SHA256 // Shutdown cleans up any leftover go routines func (n *Network) Shutdown() error { + if n.disabled { + return nil + } // Stop protocols and connection manager for _, prot := range n.protocols { prot.Stop() @@ -758,6 +790,9 @@ func (n *Network) Shutdown() error { // Diagnostics collects and returns diagnostics for the Network engine. func (n *Network) Diagnostics() []core.DiagnosticResult { + if n.disabled { + return nil + } var results = make([]core.DiagnosticResult, 0) // Connection manager and protocols results = append(results, core.DiagnosticResultMap{Title: "connections", Items: n.connectionManager.Diagnostics()}) @@ -778,6 +813,9 @@ func (n *Network) Diagnostics() []core.DiagnosticResult { // PeerDiagnostics returns a map containing diagnostic information of the node's peers. The key contains the remote peer's ID. func (n *Network) PeerDiagnostics() map[transport.PeerID]transport.Diagnostics { + if n.disabled { + return nil + } result := make(map[transport.PeerID]transport.Diagnostics, 0) // We assume higher protocol versions (later in the slice) have better/more accurate diagnostics, // so for now they're copied over diagnostics of earlier versions, unless the entry is empty for that peer. @@ -793,15 +831,25 @@ func (n *Network) PeerDiagnostics() map[transport.PeerID]transport.Diagnostics { } func (n *Network) AddressBook() []transport.Contact { + if n.disabled { + return nil + } return n.connectionManager.Contacts() } +func (n *Network) Disabled() bool { + return n.disabled +} + // ReprocessReport describes the reprocess exection. type ReprocessReport struct { // reserved for future use } func (n *Network) Reprocess(ctx context.Context, contentType string) (*ReprocessReport, error) { + if n.disabled { + return nil, ErrDIDNutsDisabled + } log.Logger().Infof("Starting reprocess of %s", contentType) _, js, err := n.eventPublisher.Pool().Acquire(ctx) diff --git a/network/network_test.go b/network/network_test.go index 9a41c23c5..e07fed42a 100644 --- a/network/network_test.go +++ b/network/network_test.go @@ -261,14 +261,13 @@ func TestNetwork_Configure(t *testing.T) { assert.EqualError(t, err, "disabling TLS in strict mode is not allowed") }) - t.Run("ok - TLS disabled in strict mode, with empty node", func(t *testing.T) { + t.Run("ok - network disabled if not in supported"+ + " DIDMethods", func(t *testing.T) { ctrl := gomock.NewController(t) ctx := createNetwork(t, ctrl) - ctx.protocol.EXPECT().Configure(gomock.Any()) - ctx.network.connectionManager = nil err := ctx.network.Configure(core.TestServerConfig(func(config *core.ServerConfig) { - config.Datadir = io.TestDirectory(t) + config.DIDMethods = []string{"web"} })) require.NoError(t, err) diff --git a/vcr/test.go b/vcr/test.go index 52062bee7..c67fbffd6 100644 --- a/vcr/test.go +++ b/vcr/test.go @@ -177,6 +177,7 @@ func newMockContext(t *testing.T) mockContext { tx.EXPECT().Subscribe("vcr_vcs", gomock.Any(), gomock.Any()) tx.EXPECT().Subscribe("vcr_revocations", gomock.Any(), gomock.Any()) tx.EXPECT().CleanupSubscriberEvents("vcr_vcs", gomock.Any()) + tx.EXPECT().Disabled().AnyTimes() didResolver := resolver.NewMockDIDResolver(ctrl) documentOwner := didsubject.NewMockDocumentOwner(ctrl) vdrInstance := vdr.NewMockVDR(ctrl) diff --git a/vcr/vcr.go b/vcr/vcr.go index 59400e5e0..ac4df93d2 100644 --- a/vcr/vcr.go +++ b/vcr/vcr.go @@ -203,7 +203,6 @@ func (c *vcr) Configure(config core.ServerConfig) error { c.keyResolver = resolver.DIDKeyResolver{Resolver: didResolver} c.serviceResolver = resolver.DIDServiceResolver{Resolver: didResolver} - networkPublisher := issuer.NewNetworkPublisher(c.network, didResolver, c.keyStore) tlsConfig, err := c.pkiProvider.CreateTLSConfig(config.TLS) // returns nil if TLS is disabled if err != nil { return err @@ -225,11 +224,18 @@ func (c *vcr) Configure(config core.ServerConfig) error { c.openidSessionStore = c.storageClient.GetSessionDatabase() } + var networkPublisher issuer.Publisher + if !c.network.Disabled() { + networkPublisher = issuer.NewNetworkPublisher(c.network, didResolver, c.keyStore) + } + status := revocation.NewStatusList2021(c.storageClient.GetSQLDatabase(), client.NewWithCache(config.HTTPClient.Timeout), config.URL) c.issuer = issuer.NewIssuer(c.issuerStore, c, networkPublisher, openidHandlerFn, didResolver, c.keyStore, c.jsonldManager, c.trustConfig, status) c.verifier = verifier.NewVerifier(c.verifierStore, didResolver, c.keyResolver, c.jsonldManager, c.trustConfig, status) - c.ambassador = NewAmbassador(c.network, c, c.verifier, c.eventManager) + if !c.network.Disabled() { + c.ambassador = NewAmbassador(c.network, c, c.verifier, c.eventManager) + } // Create holder/wallet c.wallet = holder.NewSQLWallet(c.keyResolver, c.keyStore, c.verifier, c.jsonldManager, c.storageClient) @@ -269,6 +275,9 @@ func (c *vcr) createCredentialsStore() error { } func (c *vcr) Start() error { + if c.ambassador == nil { // did:nuts / network layer is disabled + return nil + } // start listening for new credentials _ = c.ambassador.Configure() diff --git a/vcr/vcr_test.go b/vcr/vcr_test.go index 2669ecf5f..195b99ccd 100644 --- a/vcr/vcr_test.go +++ b/vcr/vcr_test.go @@ -73,7 +73,9 @@ func TestVCR_Configure(t *testing.T) { vdrInstance.EXPECT().Resolver().AnyTimes() pkiProvider := pki.NewMockProvider(ctrl) pkiProvider.EXPECT().CreateTLSConfig(gomock.Any()).Return(nil, nil).AnyTimes() - instance := NewVCRInstance(nil, vdrInstance, nil, jsonld.NewTestJSONLDManager(t), nil, storage.NewTestStorageEngine(t), pkiProvider).(*vcr) + networkInstance := network.NewMockTransactions(ctrl) + networkInstance.EXPECT().Disabled().AnyTimes() + instance := NewVCRInstance(nil, vdrInstance, networkInstance, jsonld.NewTestJSONLDManager(t), nil, storage.NewTestStorageEngine(t), pkiProvider).(*vcr) instance.config.OpenID4VCI.Enabled = true err := instance.Configure(core.TestServerConfig(func(config *core.ServerConfig) { diff --git a/vdr/vdr.go b/vdr/vdr.go index 22bea69d8..cff3e50f6 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -151,7 +151,10 @@ func (r *Module) Configure(config core.ServerConfig) error { } } - r.networkAmbassador = didnuts.NewAmbassador(r.network, r.store, r.eventManager) + // only create ambassador when did:nuts is enabled + if slices.Contains(r.supportedDIDMethods, "nuts") { + r.networkAmbassador = didnuts.NewAmbassador(r.network, r.store, r.eventManager) + } db := r.storageInstance.GetSQLDatabase() r.didResolver.(*resolver.DIDResolverRouter).Register(didjwk.MethodName, didjwk.NewResolver()) @@ -202,10 +205,17 @@ func (r *Module) Configure(config core.ServerConfig) error { r.Manager = didsubject.New(db, methodManagers, r.keyStore, r.supportedDIDMethods) // Initiate the routines for auto-updating the data. - return r.networkAmbassador.Configure() + if r.networkAmbassador != nil { + return r.networkAmbassador.Configure() + } + return nil } func (r *Module) Start() error { + // nothing to start if did:nuts is disabled + if r.networkAmbassador == nil { + return nil + } err := r.networkAmbassador.Start() if err != nil { return err