Skip to content

Commit

Permalink
migration: did:web for existing did:nuts (#3421)
Browse files Browse the repository at this point in the history
* migration: did:web for existing did:nuts

* migration run in deterministic order
  • Loading branch information
gerardsn authored Oct 1, 2024
1 parent e2ce9e0 commit 826aa34
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 34 deletions.
72 changes: 55 additions & 17 deletions e2e-tests/migration/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,32 @@ package migration

import (
"encoding/json"
did "github.com/nuts-foundation/go-did/did"
"github.com/stretchr/testify/assert"
"os"
"testing"
"time"

"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/storage/orm"
"github.com/nuts-foundation/nuts-node/vdr/didsubject"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"strings"
"testing"
)

type manager struct {
DID *didsubject.SqlDIDManager
DOC *didsubject.SqlDIDDocumentManager
}

func Test_Migrations(t *testing.T) {
db := storage.NewTestStorageEngineInDir(t, "./nodeA/data").GetSQLDatabase()
man := &manager{
DID: didsubject.NewDIDManager(db),
DOC: didsubject.NewDIDDocumentManager(db),
}

DIDs, err := didsubject.NewDIDManager(db).All()
DIDs, err := man.DID.All()
require.NoError(t, err)
require.Len(t, DIDs, 4)
require.Len(t, DIDs, 7) // 4 did:nuts, 3 did:web

t.Run("vendor", func(t *testing.T) {
// versions for did:nuts:
Expand All @@ -51,13 +59,15 @@ func Test_Migrations(t *testing.T) {
//
// total 4 versions in SQL; latest has 2 services and 2 VMs
id := did.MustParseDID(os.Getenv("VENDOR_DID"))
var doc orm.DidDocument
err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error
doc, err := man.DOC.Latest(id, nil)
require.NoError(t, err)

assert.Equal(t, 3, doc.Version)
assert.Len(t, doc.Services, 2)
assert.Len(t, doc.VerificationMethods, 2)

// migration: add did:web
EqualServices(t, man, doc)
})
t.Run("org1", func(t *testing.T) {
// versions for did:nuts:
Expand All @@ -68,8 +78,7 @@ func Test_Migrations(t *testing.T) {
//
// total 4 versions in SQL; latest one has no controller, 2 services, and 1 VM
id := did.MustParseDID(os.Getenv("ORG1_DID"))
var doc orm.DidDocument
err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error
doc, err := man.DOC.Latest(id, nil)
require.NoError(t, err)

assert.Equal(t, 3, doc.Version)
Expand All @@ -78,6 +87,9 @@ func Test_Migrations(t *testing.T) {
didDoc := new(did.Document)
require.NoError(t, json.Unmarshal([]byte(doc.Raw), didDoc))
assert.Empty(t, didDoc.Controller)

// migration: add did:web
EqualServices(t, man, doc)
})
t.Run("org2", func(t *testing.T) {
// versions for did:nuts:
Expand All @@ -88,26 +100,29 @@ func Test_Migrations(t *testing.T) {
//
// total 2 versions in SQL, migration stopped at LC5; no controller, 0 service, 0 VM
id := did.MustParseDID(os.Getenv("ORG2_DID"))
var doc orm.DidDocument
err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error
doc, err := man.DOC.Latest(id, nil)
require.NoError(t, err)

assert.Equal(t, 1, doc.Version)
assert.Len(t, doc.Services, 0)
assert.Len(t, doc.VerificationMethods, 0)

// deactivated; has no did:web
dids, err := man.DID.FindBySubject(doc.DID.Subject) // migrated documents have subject == did:nuts:...
require.NoError(t, err)
assert.Len(t, dids, 1)
})
t.Run("org3", func(t *testing.T) {
// versions for did:nuts:
// - LC3: init -> has controller
// - LC7: add service1
// - LC7: add verification method, conflicts with above
// - LC9: add service2, solves conflict
// migration removes controller, total 5 versions in SQL
// migration removes controller
//
// total 5 versions in SQL; no controller, 2 services, 2 VMs
id := did.MustParseDID(os.Getenv("ORG3_DID"))
var doc orm.DidDocument
err = db.Preload("DID").Preload("Services").Preload("VerificationMethods").Where("did = ? AND updated_at <= ?", id.String(), time.Now()).Order("version desc").First(&doc).Error
doc, err := man.DOC.Latest(id, nil)
require.NoError(t, err)

assert.Equal(t, 4, doc.Version)
Expand All @@ -116,5 +131,28 @@ func Test_Migrations(t *testing.T) {
didDoc := new(did.Document)
require.NoError(t, json.Unmarshal([]byte(doc.Raw), didDoc))
assert.Empty(t, didDoc.Controller)

// migration: add did:web
EqualServices(t, man, doc)
})
}

func EqualServices(t *testing.T, man *manager, nutsDoc *orm.DidDocument) {
didWebPrefix := "did:web:nodeA%3A8080"

dids, err := man.DID.FindBySubject(nutsDoc.DID.Subject) // migrated documents have subject == did:nuts:...
require.NoError(t, err)
assert.Len(t, dids, 2)
var webDoc *orm.DidDocument
for _, id := range dids {
if strings.HasPrefix(id.ID, "did:web:") {
webDoc, err = man.DOC.Latest(did.MustParseDID(id.ID), nil)
require.NoError(t, err)
}
}
assert.Equal(t, 0, webDoc.Version)
assert.Equal(t, len(nutsDoc.Services), len(webDoc.Services))
for _, service := range webDoc.Services {
assert.True(t, strings.HasPrefix(service.ID, didWebPrefix))
}
}
2 changes: 1 addition & 1 deletion storage/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func NewTestInMemorySessionDatabase(t *testing.T) *InMemorySessionDatabase {
func AddDIDtoSQLDB(t testing.TB, db *gorm.DB, dids ...did.DID) {
for _, id := range dids {
// use gorm EXEC since it accepts '?' as the argument placeholder for all DBs
require.NoError(t, db.Exec("INSERT INTO did (subject, id ) VALUES ( ?, ? )", id.String(), id.String(), id.String()).Error)
require.NoError(t, db.Exec("INSERT INTO did ( subject, id ) VALUES ( ?, ? )", id.String(), id.String(), id.String()).Error)
}
}

Expand Down
3 changes: 3 additions & 0 deletions vdr/didsubject/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ type DocumentMigration interface {
// It adds all versions of a DID Document up to a deactivated version. Any changes after a deactivation are not migrated.
// getHistory retrieves the history of the DID since the requested version.
MigrateDIDHistoryToSQL(id did.DID, subject string, getHistory func(id did.DID, sinceVersion int) ([]orm.MigrationDocument, error)) error
// MigrateAddWebToNuts checks if the provided did:nuts DID adds a did:web DID under the same subject if does not already have one.
// It does not check that id is a did:nuts DID
MigrateAddWebToNuts(ctx context.Context, id did.DID) error
}

// SubjectCreationOption links all create DIDs to the DID Subject
Expand Down
71 changes: 71 additions & 0 deletions vdr/didsubject/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,74 @@ func (r *SqlManager) MigrateDIDHistoryToSQL(id did.DID, subject string, getHisto
return nil
})
}

func (r *SqlManager) MigrateAddWebToNuts(ctx context.Context, id did.DID) error {
// get subject
// TODO: this should only run on migrations, so could use 'subject = id.String()'
var subject string
err := r.DB.Model(new(orm.DID)).Where("id = ?", id.String()).Select("subject").First(&subject).Error
if err != nil {
return err
}

// check if subject has a did:web
subjectDIDs, err := r.ListDIDs(ctx, subject)
for _, subjectDID := range subjectDIDs {
if subjectDID.Method == "web" {
// already has a did:web
return nil
}
}

// get latest did:nuts document
sqlDIDDocumentManager := NewDIDDocumentManager(r.DB)
nutsDoc, err := sqlDIDDocumentManager.Latest(id, nil)
if err != nil {
return err
}

// don't add did:web if did:nuts is deactivated
nutsDidDoc, err := nutsDoc.ToDIDDocument()
if err != nil {
return err
}
if resolver.IsDeactivated(nutsDidDoc) {
return nil
}

// create a did:web for this subject
webDoc, err := r.MethodManagers["web"].NewDocument(ctx, orm.AssertionKeyUsage())
if err != nil {
return err
}
// add subject
webDID := orm.DID{
ID: webDoc.DID.ID,
Subject: subject,
}
// rename services. only the DID part of the service.ID needs to be updates
webDoc.Services = make([]orm.Service, len(nutsDoc.Services))
for i, ormService := range nutsDoc.Services {
service := new(did.Service)
err = json.Unmarshal(ormService.Data, service)
if err != nil {
return err
}
service.ID = ssi.MustParseURI(webDID.ID + "#" + service.ID.Fragment)
rawService, err := json.Marshal(service)
if err != nil {
return err
}
webDoc.Services[i] = orm.Service{
ID: service.ID.String(),
Data: rawService,
}
}
// store did:web
_, err = sqlDIDDocumentManager.CreateOrUpdate(webDID, webDoc.VerificationMethods, webDoc.Services)
if err != nil {
return err
}

return nil
}
125 changes: 125 additions & 0 deletions vdr/didsubject/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,13 @@ type testMethod struct {
committed bool
error error
method string
document *orm.DidDocument
}

func (t testMethod) NewDocument(_ context.Context, _ orm.DIDKeyFlags) (*orm.DidDocument, error) {
if t.document != nil {
return t.document, nil
}
method := t.method
if method == "" {
method = "example"
Expand Down Expand Up @@ -508,6 +512,7 @@ func TestSqlManager_MigrateDIDHistoryToSQL(t *testing.T) {
ormMigrateUpdate := orm.MigrationDocument{Raw: rawDocUpdate, Created: created, Updated: created.Add(2 * time.Second), Version: 1}
ormMigrateDeactivate := orm.MigrationDocument{Raw: rawDocDeactivate, Created: created, Updated: created.Add(2 * time.Second), Version: 1}
ormDocNew, err := ormMigrateNew.ToORMDocument(subject)
require.NoError(t, err)
ormDocUpdate, _ := ormMigrateUpdate.ToORMDocument(subject)
ormDocDeactivate, _ := ormMigrateDeactivate.ToORMDocument(subject)
equal := func(t *testing.T, o1, o2 orm.DidDocument) {
Expand Down Expand Up @@ -594,3 +599,123 @@ func TestSqlManager_MigrateDIDHistoryToSQL(t *testing.T) {
assert.EqualError(t, err, "test")
})
}

func TestSqlManager_MigrateAddWebToNuts(t *testing.T) {
didNuts := did.MustParseDID("did:nuts:test")
didWeb := did.MustParseDID("did:web:example.com")
nutsDoc := generateTestORMDoc(t, didNuts, didNuts.String(), true)
webDoc := generateTestORMDoc(t, didWeb, didNuts.String(), false) // don't add service to check it gets migrated properly

var err error
auditContext := audit.Context(context.Background(), "system", "VDR", "migrate_add_did:web_to_did:nuts")

t.Run("ok", func(t *testing.T) {
db := testDB(t)
_, err = NewDIDDocumentManager(db).CreateOrUpdate(nutsDoc.DID, nutsDoc.VerificationMethods, nutsDoc.Services)
require.NoError(t, err)
m := SqlManager{DB: db, MethodManagers: map[string]MethodManager{
"web": testMethod{document: &webDoc},
}, PreferredOrder: []string{"web"}}

// create did:web
err = m.MigrateAddWebToNuts(auditContext, didNuts)
assert.NoError(t, err)

dids, err := NewDIDManager(db).FindBySubject(didNuts.String())
assert.NoError(t, err)
require.Len(t, dids, 2)
assert.Equal(t, didNuts.String(), dids[0].ID)
assert.Equal(t, didWeb.String(), dids[1].ID)

docNuts, err := NewDIDDocumentManager(db).Latest(didNuts, nil)
require.NoError(t, err)
docWeb, err := NewDIDDocumentManager(db).Latest(didWeb, nil)
require.NoError(t, err)
assert.Equal(t, len(docNuts.Services), len(docWeb.Services))
assert.Equal(t, didWeb.String()+"#service-1", docWeb.Services[0].ID)
})
t.Run("ok - already has did:web", func(t *testing.T) {
db := testDB(t)
_, err = NewDIDDocumentManager(db).CreateOrUpdate(nutsDoc.DID, nutsDoc.VerificationMethods, nutsDoc.Services)
require.NoError(t, err)
m := SqlManager{DB: db, MethodManagers: map[string]MethodManager{
"web": testMethod{document: &webDoc},
}, PreferredOrder: []string{"web"}}

// migration 1; create did:web
err = m.MigrateAddWebToNuts(auditContext, didNuts)
assert.NoError(t, err)

dids, err := NewDIDManager(db).FindBySubject(didNuts.String())
assert.NoError(t, err)
require.Len(t, dids, 2)

// migration 2; already has a did:web
err = m.MigrateAddWebToNuts(auditContext, didNuts)
assert.NoError(t, err)

dids2, err := NewDIDManager(db).FindBySubject(didNuts.String())
assert.NoError(t, err)
require.Len(t, dids2, 2)
assert.Equal(t, dids, dids2)
})
t.Run("ok - deactivated", func(t *testing.T) {
db := testDB(t)
_, err = NewDIDDocumentManager(db).CreateOrUpdate(nutsDoc.DID, nil, nil)
require.NoError(t, err)
m := SqlManager{DB: db}

// migrate is a noop
err = m.MigrateAddWebToNuts(auditContext, didNuts)
assert.NoError(t, err)

dids, err := NewDIDManager(db).FindBySubject(didNuts.String())
assert.NoError(t, err)
require.Len(t, dids, 1)
assert.Equal(t, didNuts.String(), dids[0].ID)
})
t.Run("error - did not found", func(t *testing.T) {
db := testDB(t)
m := SqlManager{DB: db}

// empty db
err = m.MigrateAddWebToNuts(auditContext, didNuts)

assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
})
t.Run("error - doc not found", func(t *testing.T) {
db := testDB(t)
storage.AddDIDtoSQLDB(t, db, didNuts) // only add did, not the doc
m := SqlManager{DB: db}

// migrate is a noop
err = m.MigrateAddWebToNuts(auditContext, didNuts)

assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
})
}

func generateTestORMDoc(t *testing.T, id did.DID, subject string, addService bool) orm.DidDocument {
// verification method
vmID := did.MustParseDIDURL(id.String() + "#key-1")
key, _ := spi.GenerateKeyPair()
vm, err := did.NewVerificationMethod(vmID, ssi.JsonWebKey2020, id, key.Public())
require.NoError(t, err)
// service
var service did.Service
if addService {
service = did.Service{
ID: ssi.MustParseURI(id.String() + "#service-1"),
Type: "test",
ServiceEndpoint: "https://example.com",
}
}
// generate and parse document
didDoc := did.Document{ID: id, VerificationMethod: did.VerificationMethods{vm}, CapabilityInvocation: []did.VerificationRelationship{{VerificationMethod: vm}}, Service: []did.Service{service}}
rawDoc, err := json.Marshal(didDoc)
require.NoError(t, err)
now := time.Now()
ormDoc, err := orm.MigrationDocument{Raw: rawDoc, Created: now, Updated: now, Version: 0}.ToORMDocument(subject)
require.NoError(t, err)
return ormDoc
}
Loading

0 comments on commit 826aa34

Please sign in to comment.