Skip to content

Commit

Permalink
Add many2many relationship for DID document services and verification…
Browse files Browse the repository at this point in the history
… methods (#3373)

* Add many2many relationship for DID document services and verification methods

* add test

* reference table after it is declared

* fix column length
  • Loading branch information
gerardsn committed Sep 16, 2024
1 parent 4de6178 commit db0d86f
Show file tree
Hide file tree
Showing 17 changed files with 121 additions and 94 deletions.
2 changes: 1 addition & 1 deletion crypto/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func newKeyReference(t *testing.T, client *Crypto, kid string) (*orm.KeyReferenc
ref, publicKey, err := client.New(audit.TestContext(), StringNamingFunc(kid))
require.NoError(t, err)
DID := orm.DID{ID: "did:test:" + t.Name(), Subject: "subject"}
DIDDoc := orm.DIDDocument{
DIDDoc := orm.DidDocument{
DID: DID,
VerificationMethods: []orm.VerificationMethod{
{
Expand Down
2 changes: 1 addition & 1 deletion storage/orm/changelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type DIDChangeLog struct {
DIDDocumentVersionID string `gorm:"primaryKey;column:did_document_version_id"`
Type string
TransactionID string `gorm:"column:transaction_id"`
DIDDocumentVersion DIDDocument `gorm:"foreignKey:DIDDocumentVersionID;references:ID"`
DIDDocumentVersion DidDocument `gorm:"foreignKey:DIDDocumentVersionID;references:ID"`
}

func (d DIDChangeLog) TableName() string {
Expand Down
8 changes: 4 additions & 4 deletions storage/orm/changelog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestDIDEventLog_DID(t *testing.T) {
t.Run("ok", func(t *testing.T) {
id := did.MustParseDID("did:example:123")
didEventLog := DIDChangeLog{
DIDDocumentVersion: DIDDocument{
DIDDocumentVersion: DidDocument{
DID: DID{ID: id.String()},
},
}
Expand All @@ -38,7 +38,7 @@ func TestDIDEventLog_DID(t *testing.T) {
})
t.Run("malformed DID", func(t *testing.T) {
didEventLog := DIDChangeLog{
DIDDocumentVersion: DIDDocument{
DIDDocumentVersion: DidDocument{
DID: DID{ID: "malformed"},
},
}
Expand All @@ -51,7 +51,7 @@ func TestDIDEventLog_Method(t *testing.T) {
t.Run("ok", func(t *testing.T) {
id := did.MustParseDID("did:example:123")
didEventLog := DIDChangeLog{
DIDDocumentVersion: DIDDocument{
DIDDocumentVersion: DidDocument{
DID: DID{ID: id.String()},
},
}
Expand All @@ -60,7 +60,7 @@ func TestDIDEventLog_Method(t *testing.T) {
})
t.Run("malformed DID", func(t *testing.T) {
didEventLog := DIDChangeLog{
DIDDocumentVersion: DIDDocument{
DIDDocumentVersion: DidDocument{
DID: DID{ID: "malformed"},
},
}
Expand Down
14 changes: 7 additions & 7 deletions storage/orm/did_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import (
"gorm.io/gorm/schema"
)

// DIDDocument is the gorm representation of the did_document_version table
type DIDDocument struct {
// DidDocument is the gorm representation of the did_document_version table
type DidDocument struct {
ID string `gorm:"primaryKey"`
DidID string `gorm:"column:did"`
DID DID `gorm:"foreignKey:DidID;references:ID"`
Expand All @@ -36,19 +36,19 @@ type DIDDocument struct {
// Also used to purge DID document changes that haven't been committed within a certain time frame
UpdatedAt int64 `gorm:"autoUpdateTime:false"`
Version int
VerificationMethods []VerificationMethod `gorm:"foreignKey:DIDDocumentID;references:ID"`
Services []Service `gorm:"foreignKey:DIDDocumentID;references:ID"`
VerificationMethods []VerificationMethod `gorm:"many2many:did_document_to_verification_method"`
Services []Service `gorm:"many2many:did_document_to_service"`
// Raw contains the DID Document as generated by the specific method, important for hashing.
Raw string
}

func (d DIDDocument) TableName() string {
func (d DidDocument) TableName() string {
return "did_document_version"
}

var _ schema.Tabler = (*DID)(nil)

func (sqlDoc DIDDocument) ToDIDDocument() (did.Document, error) {
func (sqlDoc DidDocument) ToDIDDocument() (did.Document, error) {
if len(sqlDoc.Raw) > 0 {
document := did.Document{}
err := json.Unmarshal([]byte(sqlDoc.Raw), &document)
Expand All @@ -60,7 +60,7 @@ func (sqlDoc DIDDocument) ToDIDDocument() (did.Document, error) {
return sqlDoc.GenerateDIDDocument()
}

func (sqlDoc DIDDocument) GenerateDIDDocument() (did.Document, error) {
func (sqlDoc DidDocument) GenerateDIDDocument() (did.Document, error) {
id, _ := did.ParseDID(sqlDoc.DID.ID)
others := make([]ssi.URI, 0)
for _, alias := range sqlDoc.DID.Aka {
Expand Down
2 changes: 1 addition & 1 deletion storage/orm/did_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestDIDDocument_ToDIDDocument(t *testing.T) {
ID: "#2",
Data: []byte(serviceData),
}
document := DIDDocument{
document := DidDocument{
ID: "id",
DID: DID{ID: alice.String(), Aka: []DID{{ID: bob.String()}}},
Version: 1,
Expand Down
5 changes: 2 additions & 3 deletions storage/orm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ var _ schema.Tabler = (*Service)(nil)

// Service is the gorm representation of the did_service table
type Service struct {
ID string `gorm:"primaryKey"`
DIDDocumentID string `gorm:"column:did_document_id"`
Data []byte
ID string `gorm:"primaryKey"`
Data []byte
}

func (v Service) TableName() string {
Expand Down
15 changes: 6 additions & 9 deletions storage/orm/verification_method.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,20 @@

package orm

import (
"gorm.io/gorm/schema"
)
import "gorm.io/gorm/schema"

var _ schema.Tabler = (*VerificationMethod)(nil)

// VerificationMethod is the gorm representation of the did_verificationmethod table
type VerificationMethod struct {
ID string `gorm:"primaryKey"`
DIDDocumentID string `gorm:"column:did_document_id"`
KeyTypes VerificationMethodKeyType
Weight int16
Data []byte
ID string `gorm:"primaryKey"`
KeyTypes VerificationMethodKeyType
Weight int16
Data []byte
}

func (v VerificationMethod) TableName() string {
return "did_verificationmethod"
return "did_verification_method"
}

// VerificationMethodKeyType is used to marshal and unmarshal the key type to the DB
Expand Down
41 changes: 29 additions & 12 deletions storage/sql_migrations/003_did.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ create table did_document_version
version int not null,
raw $TEXT_TYPE, -- make not nil in future PR
unique (did, version),
foreign key (did) references did (id) on delete cascade,
unique (did, version)
foreign key (did) references did (id) on delete cascade
);

-- this table is used for the poor-mans 2-phase commit
Expand All @@ -49,12 +48,10 @@ create table key_reference
);

-- this table is used to store the verification methods for locally managed DIDs
create table did_verificationmethod
create table did_verification_method
(
-- id is the unique id of the verification method as it appears in the DID document using the fully qualified representation.
id varchar(415) not null primary key,
-- did_document_id references the DID document version
did_document_id varchar(36) not null,
-- key_types is a base64 encoded bitmask of the key types supported by the verification method.
-- 0x01 - AssertionMethod
-- 0x02 - Authentication
Expand All @@ -67,25 +64,45 @@ create table did_verificationmethod
weight SMALLINT default 0,
-- data is a JSON object containing the verification method data, e.g. the public key.
-- When producing the verificationMethod, data is used as JSON base object and the id and type are added.
data $TEXT_TYPE not null,
foreign key (did_document_id) references did_document_version (id) on delete cascade
data $TEXT_TYPE not null
);

-- this table is used to link unique verification methods to all DID document versions they are used in
create table did_document_to_verification_method
(
-- did_document_id references the DID document version
did_document_id varchar(36) not null,
-- verification_method_id references the verification method
verification_method_id varchar(415) not null,
primary key (did_document_id,verification_method_id),
foreign key (did_document_id) references did_document_version (id) on delete cascade,
foreign key (verification_method_id) references did_verification_method (id) on delete cascade
);

-- this table is used to store the services for locally managed DIDs
create table did_service
(
-- id is the unique id of the service as it appears in the DID document using the shorthand representation.
id varchar(254) not null primary key,
-- did_document_id references the DID document version
did_document_id varchar(36) not null,
-- data is a JSON object containing the service data, e.g. the serviceEndpoint.
-- When producing the service, data is used as JSON base object and the id and type are added.
data $TEXT_TYPE not null,
foreign key (did_document_id) references did_document_version (id) on delete cascade
data $TEXT_TYPE not null
);

-- this table is used to link unique services to all DID document versions they are used in
create table did_document_to_service
(
-- did_document_id references the DID document version
did_document_id varchar(36) not null,
-- service_id references the DID service
service_id varchar(254) not null,
primary key (did_document_id,service_id),
foreign key (did_document_id) references did_document_version (id) on delete cascade,
foreign key (service_id) references did_service (id) on delete cascade
);

-- +goose Down
drop table did_verificationmethod;
drop table did_verification_method;
drop table did_service;
drop table did_change_log;
drop table did_document_version;
Expand Down
4 changes: 2 additions & 2 deletions vdr/didnuts/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ func (m Manager) Update(ctx context.Context, id did.DID, next did.Document) erro
* New style DID Method Manager
******************************/

func (m Manager) NewDocument(ctx context.Context, _ orm.DIDKeyFlags) (*orm.DIDDocument, error) {
func (m Manager) NewDocument(ctx context.Context, _ orm.DIDKeyFlags) (*orm.DidDocument, error) {
keyRef, publicKey, err := m.keyStore.New(ctx, DIDKIDNamingFunc)
if err != nil {
return nil, err
Expand All @@ -272,7 +272,7 @@ func (m Manager) NewDocument(ctx context.Context, _ orm.DIDKeyFlags) (*orm.DIDDo
}
vmAsJson, _ := json.Marshal(verificationMethod)
now := time.Now().Unix()
sqlDoc := orm.DIDDocument{
sqlDoc := orm.DidDocument{
DID: orm.DID{
ID: keyID.DID.String(),
},
Expand Down
4 changes: 2 additions & 2 deletions vdr/didnuts/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ func TestManager_Commit(t *testing.T) {
data, _ := json.Marshal(document.VerificationMethod[0])
return orm.DIDChangeLog{
Type: orm.DIDChangeCreated,
DIDDocumentVersion: orm.DIDDocument{
DIDDocumentVersion: orm.DidDocument{
ID: uuid.New().String(),
DID: orm.DID{
ID: document.ID.String(),
Expand Down Expand Up @@ -418,7 +418,7 @@ func TestManager_IsCommitted(t *testing.T) {
vmData, _ := json.Marshal(document.VerificationMethod[0])
eventLog := orm.DIDChangeLog{
Type: orm.DIDChangeCreated,
DIDDocumentVersion: orm.DIDDocument{
DIDDocumentVersion: orm.DidDocument{
ID: uuid.New().String(),
DID: orm.DID{
ID: document.ID.String(),
Expand Down
31 changes: 11 additions & 20 deletions vdr/didsubject/did_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ type DIDDocumentManager interface {
// If the DID does not exist yet, it will be created
// It adds all verification methods, services, alsoKnownAs to the DID document
// Not passing any verification methods will create an empty DID document, deactivation checking should be done by the caller
CreateOrUpdate(did orm.DID, verificationMethods []orm.VerificationMethod, services []orm.Service) (*orm.DIDDocument, error)
CreateOrUpdate(did orm.DID, verificationMethods []orm.VerificationMethod, services []orm.Service) (*orm.DidDocument, error)
// Latest returns the latest version of a DID document
// if notAfter is given, it will return the latest version before that time
Latest(did did.DID, notAfter *time.Time) (*orm.DIDDocument, error)
Latest(did did.DID, notAfter *time.Time) (*orm.DidDocument, error)
}

// SqlDIDDocumentManager is the implementation of the DIDDocumentManager interface
Expand All @@ -53,28 +53,19 @@ func NewDIDDocumentManager(tx *gorm.DB) *SqlDIDDocumentManager {
return &SqlDIDDocumentManager{tx: tx}
}

func (s *SqlDIDDocumentManager) CreateOrUpdate(did orm.DID, verificationMethods []orm.VerificationMethod, services []orm.Service) (*orm.DIDDocument, error) {
latest := orm.DIDDocument{Version: -1} // -1 means no document exists, will be overwritten below if there is a document
func (s *SqlDIDDocumentManager) CreateOrUpdate(did orm.DID, verificationMethods []orm.VerificationMethod, services []orm.Service) (*orm.DidDocument, error) {
latest := orm.DidDocument{Version: -1} // -1 means no document exists, will be overwritten below if there is a document
err := s.tx.Preload("DID").Where("did = ?", did.ID).Order("version desc").First(&latest).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
version := latest.Version + 1
id := uuid.New().String()
// update DIDDocumentID for all VMs and services
for i := range verificationMethods {
verificationMethods[i].DIDDocumentID = id
}
for i := range services {
services[i].DIDDocumentID = id
}
now := time.Now().Unix()
doc := orm.DIDDocument{
ID: id,

doc := orm.DidDocument{
ID: uuid.New().String(),
DID: did,
CreatedAt: latest.CreatedAt,
UpdatedAt: now,
Version: version,
UpdatedAt: time.Now().Unix(),
Version: latest.Version + 1,
VerificationMethods: verificationMethods,
Services: services,
}
Expand All @@ -87,8 +78,8 @@ func (s *SqlDIDDocumentManager) CreateOrUpdate(did orm.DID, verificationMethods
return &doc, err
}

func (s *SqlDIDDocumentManager) Latest(did did.DID, resolveTime *time.Time) (*orm.DIDDocument, error) {
doc := orm.DIDDocument{}
func (s *SqlDIDDocumentManager) Latest(did did.DID, resolveTime *time.Time) (*orm.DidDocument, error) {
doc := orm.DidDocument{}
notAfter := time.Now().Add(time.Hour).Unix()
if resolveTime != nil {
notAfter = resolveTime.Unix()
Expand Down
43 changes: 33 additions & 10 deletions vdr/didsubject/did_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ var sqlDidAlice = orm.DID{ID: alice.String(), Subject: "alice"}
func TestSqlDIDDocumentManager_CreateOrUpdate(t *testing.T) {
keyUsageFlag := orm.VerificationMethodKeyType(31)
vm := orm.VerificationMethod{
ID: "#1",
ID: "#key-1",
Data: []byte("{}"),
KeyTypes: keyUsageFlag,
}
service := orm.Service{
ID: "#2",
ID: "#service-1",
Data: []byte("{}"),
}
sqlDidBob := orm.DID{ID: bob.String(), Subject: "bob"}
Expand Down Expand Up @@ -74,31 +74,54 @@ func TestSqlDIDDocumentManager_CreateOrUpdate(t *testing.T) {
assert.Equal(t, []byte("{}"), doc.VerificationMethods[0].Data)
assert.Equal(t, keyUsageFlag, doc.VerificationMethods[0].KeyTypes)
assert.Equal(t, []byte("{}"), doc.Services[0].Data)

})
t.Run("update", func(t *testing.T) {
tx := db.Begin()
docManager := NewDIDDocumentManager(tx)
countRows := func(t *testing.T, table interface{}) int {
var count int64
err := db.Model(table).Count(&count).Error
if err != nil {
t.Fatal(err)
}
return int(count)
}
vm2 := orm.VerificationMethod{
ID: "#key-2",
Data: []byte("{}"),
KeyTypes: keyUsageFlag,
}
service2 := orm.Service{
ID: "#service-2",
Data: []byte("{}"),
}

docManager := NewDIDDocumentManager(db)
docRoot, err := docManager.CreateOrUpdate(sqlDidBob, []orm.VerificationMethod{vm}, []orm.Service{service})
require.NoError(t, err)
require.NoError(t, tx.Commit().Error)

// rewrite timestamps to make sure docRoot.CreatedAt < doc.UpdatedAt
createdAt := time.Now().Add(-2 * time.Second).Unix()
docRoot.CreatedAt, docRoot.UpdatedAt = createdAt, createdAt
db.Save(&docRoot)

docManager = NewDIDDocumentManager(transaction(t, db))
// update service
doc, err := docManager.CreateOrUpdate(sqlDidBob, []orm.VerificationMethod{vm}, []orm.Service{service2})
require.NoError(t, err)
require.Len(t, doc.Services, 1)
assert.Equal(t, 2, countRows(t, &orm.Service{}))
assert.Equal(t, 1, countRows(t, &orm.VerificationMethod{}))

doc, err := docManager.CreateOrUpdate(sqlDidBob, []orm.VerificationMethod{vm}, []orm.Service{service})
// update vm, re-add service1
doc, err = docManager.CreateOrUpdate(sqlDidBob, []orm.VerificationMethod{vm2}, []orm.Service{service, service2})
require.NoError(t, err)
assert.Equal(t, 2, countRows(t, &orm.Service{}))
assert.Equal(t, 2, countRows(t, &orm.VerificationMethod{}))

assert.Len(t, doc.ID, 36) // uuid v4
require.Len(t, doc.VerificationMethods, 1)
require.Len(t, doc.Services, 1)
require.Len(t, doc.Services, 2)
assert.Equal(t, docRoot.CreatedAt, doc.CreatedAt)
assert.Less(t, doc.CreatedAt, doc.UpdatedAt)
assert.Equal(t, 1, doc.Version)
assert.Equal(t, 2, doc.Version)
})
}

Expand Down
Loading

0 comments on commit db0d86f

Please sign in to comment.