Skip to content

Commit

Permalink
Configure backup shelves for credentials and revocations (#2341)
Browse files Browse the repository at this point in the history
* Configure backup shelves for credentials and revocations

* Add backup restore tests for other vcr DBs
  • Loading branch information
woutslakhorst authored Jul 13, 2023
1 parent 99bb59b commit dff8ac2
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 55 deletions.
25 changes: 4 additions & 21 deletions docs/pages/deployment/backup-restore.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,11 @@ To restore a backup, follow the following steps:

1. shutdown the node
2. remove the following directories from the ``datadir``: ``events``, ``network``, ``vcr``, and ``vdr``
3. keep node offline by removing ``network.bootstrapnodes`` and ``network.grpcaddr`` from the config file and set ``network.enablediscovery`` to ``false``
4. follow the restore procedure for your storage (BBolt, Redis, Hashicorp Vault)
5. restore the ``vcr/trusted_issuers.yaml`` file inside ``datadir``
6. start your node
7. make empty POST calls to

.. code-block:: http
POST <node-address>/internal/network/v1/reprocess?type=application/vc+json
POST <node-address>/internal/network/v1/reprocess?type=application/ld+json;type=revocation
.. note::

When making the API calls, make sure you use the proper URL escaping (``%2B`` for ``+`` and ``%3B`` for ``;``).
Reprocess calls return immediately and will do the work in the background.

8. wait until the reprocess operations are complete
9. shutdown the node
10. undo the configuration changes in step 3
11. start the node
3. follow the restore procedure for your storage (BBolt, Redis, Hashicorp Vault)
4. restore the ``vcr/trusted_issuers.yaml`` file inside ``datadir``
5. start your node

BBolt
=====

In step 4, copy ``network/data.db``, ``vcr/backup-issued-credentials.db`` and ``vdr/didstore.db`` from your backup to the ``datadir`` (keep the directory structure).
In step 3, copy ``network/data.db``, ``vcr/backup-credentials.db``, ``vcr/backup-issued-credentials.db``, ``vcr/backup-revoked-credentials.db`` and ``vdr/didstore.db`` from your backup to the ``datadir`` (keep the directory structure).
4 changes: 1 addition & 3 deletions vcr/issuer/leia_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ type leiaIssuerStore struct {
issuedCredentials leia.Collection
revokedCredentials leia.Collection
store storage.KVBackedLeiaStore
backupStore stoabs.KVStore
}

// NewLeiaIssuerStore creates a new instance of leiaIssuerStore which implements the Store interface.
Expand All @@ -53,7 +52,7 @@ func NewLeiaIssuerStore(dbPath string, backupStore stoabs.KVStore) (Store, error
if err != nil {
return nil, fmt.Errorf("failed to create leiaIssuerStore: %w", err)
}
// add wraper
// add backup wrapper
kvBackedStore, err := storage.NewKVBackedLeiaStore(store, backupStore)
if err != nil {
return nil, fmt.Errorf("failed to create KV backed issuer store: %w", err)
Expand Down Expand Up @@ -81,7 +80,6 @@ func NewLeiaIssuerStore(dbPath string, backupStore stoabs.KVStore) (Store, error
issuedCredentials: issuedCollection,
revokedCredentials: revokedCollection,
store: kvBackedStore,
backupStore: backupStore,
}

// initialize indices, this is required for handleRestore. Without the index metadata it can't iterate and detect data entries.
Expand Down
22 changes: 22 additions & 0 deletions vcr/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ func NewTestVCRInstance(t *testing.T) *vcr {
return newInstance
}

func NewTestVCRInstanceInDir(t *testing.T, testDirectory string) *vcr {
// give network a subdirectory to avoid duplicate networks in tests
newInstance := NewVCRInstance(
nil,
nil,
network.NewTestNetworkInstance(t),
jsonld.NewTestJSONLDManager(t),
events.NewTestManager(t),
storage.NewTestStorageEngine(testDirectory),
nil, nil,
).(*vcr)

if err := newInstance.Configure(core.TestServerConfig(core.ServerConfig{Datadir: testDirectory})); err != nil {
t.Fatal(err)
}
if err := newInstance.Start(); err != nil {
t.Fatal(err)
}

return newInstance
}

type mockContext struct {
ctrl *gomock.Controller
tx *network.MockTransactions
Expand Down
54 changes: 43 additions & 11 deletions vcr/vcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import (
"gopkg.in/yaml.v3"
)

const credentialsBackupShelf = "credentials"

// NewVCRInstance creates a new vcr instance with default config and empty concept registry
func NewVCRInstance(keyStore crypto.KeyStore, store didstore.Store,
network network.Transactions, jsonldManager jsonld.JSONLD, eventManager events.Event, storageClient storage.Engine,
Expand Down Expand Up @@ -82,7 +84,7 @@ type vcr struct {
// strictmode holds a copy of the core.ServerConfig.Strictmode value
strictmode bool
config Config
store leia.Store
store storage.KVBackedLeiaStore
didstore didstore.Store
keyStore crypto.KeyStore
docResolver vdr.DocResolver
Expand Down Expand Up @@ -167,6 +169,7 @@ func (c *vcr) Configure(config core.ServerConfig) error {
// copy strictmode for openid4vci usage
c.strictmode = config.Strictmode

// create issuer store (to revoke)
issuerStorePath := path.Join(c.datadir, "vcr", "issued-credentials.db")
issuerBackupStore, err := c.storageClient.GetProvider(ModuleName).GetKVStore("backup-issued-credentials", storage.PersistentStorageClass)
if err != nil {
Expand All @@ -177,12 +180,22 @@ func (c *vcr) Configure(config core.ServerConfig) error {
return err
}

// create verifier store (for revocations)
verifierStorePath := path.Join(c.datadir, "vcr", "verifier-store.db")
c.verifierStore, err = verifier.NewLeiaVerifierStore(verifierStorePath)
verifierBackupStore, err := c.storageClient.GetProvider(ModuleName).GetKVStore("backup-revoked-credentials", storage.PersistentStorageClass)
if err != nil {
return err
}
c.verifierStore, err = verifier.NewLeiaVerifierStore(verifierStorePath, verifierBackupStore)
if err != nil {
return err
}

// create credentials store (for public credentials and this node's wallet)
if err = c.createCredentialsStore(); err != nil {
return err
}

// create trust config
tcPath := path.Join(config.Datadir, "vcr", "trusted_issuers.yaml")
c.trustConfig = trust.NewConfig(tcPath)
Expand Down Expand Up @@ -224,28 +237,47 @@ func (c *vcr) Configure(config core.ServerConfig) error {

c.holder = holder.New(c.keyResolver, c.keyStore, c.verifier, c.jsonldManager)

if err = c.store.HandleRestore(); err != nil {
return err
}

return c.trustConfig.Load()
}

func (c *vcr) credentialsDBPath() string {
return path.Join(c.datadir, "vcr", "credentials.db")
}

func (c *vcr) Start() error {
var err error

// setup DB connection
if c.store, err = leia.NewStore(c.credentialsDBPath(), leia.WithDocumentLoader(c.jsonldManager.DocumentLoader())); err != nil {
func (c *vcr) createCredentialsStore() error {
credentialsStorePath := path.Join(c.datadir, "vcr", "credentials.db")
credentialsBackupStore, err := c.storageClient.GetProvider(ModuleName).GetKVStore("backup-credentials", storage.PersistentStorageClass)
if err != nil {
return err
}

// init indices
if err = c.initJSONLDIndices(); err != nil {
credentialsStore, err := leia.NewStore(credentialsStorePath, leia.WithDocumentLoader(c.jsonldManager.DocumentLoader()))
if err != nil {
return err
}
c.store, err = storage.NewKVBackedLeiaStore(credentialsStore, credentialsBackupStore)
if err != nil {
return err
}

// set backup config
c.store.AddConfiguration(storage.LeiaBackupConfiguration{
CollectionName: "credentials",
CollectionType: leia.JSONLDCollection,
BackupShelf: credentialsBackupShelf,
SearchQuery: leia.NewIRIPath(),
})

// init indices
return c.initJSONLDIndices()
}

func (c *vcr) Start() error {
// start listening for new credentials
c.ambassador.Configure()
_ = c.ambassador.Configure()

return c.ambassador.Start()
}
Expand Down
42 changes: 33 additions & 9 deletions vcr/vcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ package vcr

import (
"context"
"crypto/sha1"
"encoding/json"
"errors"
"github.com/nuts-foundation/go-leia/v4"
"github.com/nuts-foundation/go-stoabs"
bbolt2 "github.com/nuts-foundation/go-stoabs/bbolt"
"github.com/nuts-foundation/nuts-node/pki"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
"github.com/stretchr/testify/require"
"os"
"path"
"strings"
"testing"
"time"
Expand All @@ -51,6 +55,13 @@ import (
)

func TestVCR_Configure(t *testing.T) {
t.Run("error - creating issuer store", func(t *testing.T) {
testDirectory := io.TestDirectory(t)
instance := NewVCRInstance(nil, nil, nil, jsonld.NewTestJSONLDManager(t), nil, storage.NewTestStorageEngine(testDirectory), nil, nil).(*vcr)

err := instance.Configure(core.TestServerConfig(core.ServerConfig{Datadir: "test"}))
assert.EqualError(t, err, "failed to create leiaIssuerStore: mkdir test/vcr: not a directory")
})
t.Run("openid4vci", func(t *testing.T) {
testDirectory := io.TestDirectory(t)
ctrl := gomock.NewController(t)
Expand Down Expand Up @@ -94,15 +105,6 @@ func TestVCR_Configure(t *testing.T) {

func TestVCR_Start(t *testing.T) {

t.Run("error - creating db", func(t *testing.T) {
testDirectory := io.TestDirectory(t)
instance := NewVCRInstance(nil, nil, nil, jsonld.NewTestJSONLDManager(t), nil, storage.NewTestStorageEngine(testDirectory), nil, nil).(*vcr)

_ = instance.Configure(core.TestServerConfig(core.ServerConfig{Datadir: "test"}))
err := instance.Start()
assert.EqualError(t, err, "mkdir test/vcr: not a directory")
})

t.Run("ok", func(t *testing.T) {
instance := NewTestVCRInstance(t)

Expand Down Expand Up @@ -383,6 +385,28 @@ func TestVcr_Untrusted(t *testing.T) {
})
}

func TestVcr_restoreFromShelf(t *testing.T) {
testVC := vc.VerifiableCredential{}
_ = json.Unmarshal([]byte(jsonld.TestOrganizationCredential), &testVC)
testDir := io.TestDirectory(t)
backupStorePath := path.Join(testDir, "data", "vcr", "backup-credentials.db")
backupStore, err := bbolt2.CreateBBoltStore(backupStorePath)
require.NoError(t, err)
bytes := []byte(jsonld.TestOrganizationCredential)
ref := sha1.Sum(bytes)
_ = backupStore.WriteShelf(context.Background(), credentialsBackupShelf, func(writer stoabs.Writer) error {
return writer.Put(stoabs.BytesKey(ref[:]), bytes)
})
_ = backupStore.Close(context.Background())

store := NewTestVCRInstanceInDir(t, testDir)
_ = store.Trust(testVC.Type[0], testVC.Issuer)
require.NoError(t, err)
result, err := store.Resolve(*testVC.ID, nil)
require.NoError(t, err)
assert.Equal(t, testVC, *result)
}

func confirmUntrustedStatus(t *testing.T, fn func(issuer ssi.URI) ([]ssi.URI, error), numUntrusted int) {
trusted, err := fn(ssi.MustParseURI("NutsOrganizationCredential"))

Expand Down
32 changes: 28 additions & 4 deletions vcr/verifier/leia_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,59 @@ import (
"encoding/json"
"fmt"
"github.com/nuts-foundation/go-leia/v4"
"github.com/nuts-foundation/go-stoabs"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr/log"

ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/nuts-node/vcr/credential"
)

const revocationBackupShelf = "revocations"

// leiaVerifierStore implements the verifier Store interface. It is a simple and fast JSON store.
// Note: It can not be used in a clustered setup.
type leiaVerifierStore struct {
// revocations is a leia collection containing all the revocations
revocations leia.Collection
store leia.Store
store storage.KVBackedLeiaStore
}

// NewLeiaVerifierStore creates a new instance of leiaVerifierStore which implements the Store interface.
func NewLeiaVerifierStore(dbPath string) (Store, error) {
func NewLeiaVerifierStore(dbPath string, backupStore stoabs.KVStore) (Store, error) {
store, err := leia.NewStore(dbPath)
if err != nil {
return nil, fmt.Errorf("failed to create leiaVerifierStore: %w", err)
}

// add backup wrapper
kvBackedStore, err := storage.NewKVBackedLeiaStore(store, backupStore)
if err != nil {
return nil, fmt.Errorf("failed to create KV backed issuer store: %w", err)
}

// set backup config
kvBackedStore.AddConfiguration(storage.LeiaBackupConfiguration{
CollectionName: "revocations",
CollectionType: leia.JSONCollection,
BackupShelf: revocationBackupShelf,
SearchQuery: leia.NewJSONPath(credential.RevocationSubjectPath),
})

revocations := store.Collection(leia.JSONCollection, "revocations")
newLeiaStore := &leiaVerifierStore{
revocations: revocations,
store: store,
store: kvBackedStore,
}
if err := newLeiaStore.createIndices(revocations); err != nil {
if err = newLeiaStore.createIndices(revocations); err != nil {
return nil, err
}

if err = kvBackedStore.HandleRestore(); err != nil {
return nil, err
}

return newLeiaStore, nil
}

Expand Down
Loading

0 comments on commit dff8ac2

Please sign in to comment.