Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VDR: SubjectManager returns DIDs in preferred order #3291

Merged
merged 11 commits into from
Sep 13, 2024
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ The following options can be configured on the server:
storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session 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').
**VDR**
vdr.didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix).
vdr.didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). DIDs returned by the API are also returned in this order.
**policy**
policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping.
======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/deployment/cli-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The following options apply to the server commands below:
--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)
--vdr.didmethods strings Comma-separated list of enabled DID methods (without did: prefix). (default [web,nuts])
--vdr.didmethods strings Comma-separated list of enabled DID methods (without did: prefix). DIDs returned by the API are also returned in this order. (default [web,nuts])
--verbosity string Log level (trace, debug, info, warn, error) (default "info")

nuts config
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/deployment/server_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session 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').
**VDR**
vdr.didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix).
vdr.didmethods [web,nuts] Comma-separated list of enabled DID methods (without did: prefix). DIDs returned by the API are also returned in this order.
**policy**
policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping.
======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================
2 changes: 1 addition & 1 deletion vdr/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func FlagSet() *pflag.FlagSet {

defs := vdr.DefaultConfig()

flagSet.StringSlice("vdr.didmethods", defs.DIDMethods, "Comma-separated list of enabled DID methods (without did: prefix).")
flagSet.StringSlice("vdr.didmethods", defs.DIDMethods, "Comma-separated list of enabled DID methods (without did: prefix). DIDs returned by the API are also returned in this order.")
return flagSet
}

Expand Down
45 changes: 45 additions & 0 deletions vdr/didsubject/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ import (
"github.com/nuts-foundation/nuts-node/vdr/log"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"gorm.io/gorm"
"sort"
"time"
)

type Manager struct {
DB *gorm.DB
MethodManagers map[string]MethodManager
// PreferredOrder is the order in which the methods are preferred, which dictates the order in which they are returned.
PreferredOrder []string
}

func (r *Manager) List(_ context.Context, subject string) ([]did.DID, error) {
Expand All @@ -54,6 +57,7 @@ func (r *Manager) List(_ context.Context, subject string) ([]did.DID, error) {
}
result[i] = *id
}
sortDIDs(result, r.PreferredOrder)
return result, nil
}

Expand Down Expand Up @@ -146,6 +150,7 @@ func (r *Manager) Create(ctx context.Context, options CreationOptions) ([]did.Do
}
docs = append(docs, doc)
}
sortDIDDocuments(docs, r.PreferredOrder)
return docs, subject, nil
}

Expand Down Expand Up @@ -522,3 +527,43 @@ func (r *Manager) Rollback(ctx context.Context) {
log.Logger().WithError(err).Error("failed to rollback DID documents")
}
}

func sortDIDs(list []did.DID, order []string) {
sort.Slice(list, func(i, j int) bool {
iOrder := -1
jOrder := -1
for k, v := range order {
if v == list[i].Method {
iOrder = k
}
if v == list[j].Method {
jOrder = k
}
}
// If both are -1, they are not in the preferred order list, so sort by method for stable order
if iOrder == -1 && jOrder == -1 {
return list[i].Method < list[j].Method
}
return iOrder < jOrder
})
}

func sortDIDDocuments(list []did.Document, order []string) {
listOfDIDs := make([]did.DID, len(list))
for i, doc := range list {
listOfDIDs[i] = doc.ID
}
sortDIDs(listOfDIDs, order)
// order list according to listOfDIDs
orderedList := make([]did.Document, len(list))
for i, id := range listOfDIDs {
inner:
for _, doc := range list {
if doc.ID == id {
orderedList[i] = doc
break inner
}
}
}
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
copy(list, orderedList)
}
52 changes: 48 additions & 4 deletions vdr/didsubject/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ func TestManager_Create(t *testing.T) {
db := testDB(t)
m := Manager{DB: db, MethodManagers: map[string]MethodManager{
"example": testMethod{},
"test": testMethod{},
}}
"test": testMethod{method: "test"},
}, PreferredOrder: []string{"test", "example"}}

documents, _, err := m.Create(audit.TestContext(), DefaultCreationOptions())
require.NoError(t, err)
Expand All @@ -73,7 +73,7 @@ func TestManager_Create(t *testing.T) {
for i, document := range documents {
IDs[i] = document.ID.String()
}
assert.True(t, strings.HasPrefix(IDs[0], "did:example:"))
assert.True(t, strings.HasPrefix(IDs[0], "did:test:"))
assert.True(t, strings.HasPrefix(IDs[1], "did:example:"))

// test alsoKnownAs requirements
Expand Down Expand Up @@ -105,6 +105,28 @@ func TestManager_Create(t *testing.T) {
})
}

func TestManager_List(t *testing.T) {
t.Run("ok", func(t *testing.T) {
db := testDB(t)
m := Manager{DB: db, MethodManagers: map[string]MethodManager{
"method1": testMethod{method: "method1"},
"method2": testMethod{method: "method2"},
}, PreferredOrder: []string{"method2", "method1"}}
opts := DefaultCreationOptions().With(SubjectCreationOption{Subject: "subject"})
_, subject, err := m.Create(audit.TestContext(), opts)
require.NoError(t, err)

dids, err := m.List(audit.TestContext(), subject)

require.NoError(t, err)
require.Len(t, dids, 2)
t.Run("preferred order", func(t *testing.T) {
assert.True(t, strings.HasPrefix(dids[0].String(), "did:method2:"))
assert.True(t, strings.HasPrefix(dids[1].String(), "did:method1:"))
})
})
}

func TestManager_Services(t *testing.T) {
db := testDB(t)
m := Manager{DB: db, MethodManagers: map[string]MethodManager{
Expand Down Expand Up @@ -373,10 +395,15 @@ func TestNewIDForService(t *testing.T) {
type testMethod struct {
committed bool
error error
method string
}

func (t testMethod) NewDocument(_ context.Context, _ orm.DIDKeyFlags) (*orm.DIDDocument, error) {
id := fmt.Sprintf("did:example:%s", uuid.New().String())
method := t.method
if method == "" {
method = "example"
}
id := fmt.Sprintf("did:%s:%s", method, uuid.New().String())
return &orm.DIDDocument{DID: orm.DID{ID: id}}, t.error
}

Expand All @@ -393,3 +420,20 @@ func (t testMethod) Commit(_ context.Context, _ orm.DIDChangeLog) error {
func (t testMethod) IsCommitted(_ context.Context, _ orm.DIDChangeLog) (bool, error) {
return t.committed, t.error
}

func Test_sortDIDDocuments(t *testing.T) {
t.Run("duplicate", func(t *testing.T) {
documents := []did.Document{
{ID: did.MustParseDID("did:example:1")},
{ID: did.MustParseDID("did:example:1")},
{ID: did.MustParseDID("did:test:1")},
}

sortDIDDocuments(documents, []string{"test", "example"})

require.Len(t, documents, 3)
assert.Equal(t, "did:test:1", documents[0].ID.String())
assert.Equal(t, "did:example:1", documents[1].ID.String())
assert.Equal(t, "did:example:1", documents[2].ID.String())
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
})
}
2 changes: 1 addition & 1 deletion vdr/vdr.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (r *Module) Configure(config core.ServerConfig) error {
r.didResolver.(*resolver.DIDResolverRouter).Register(didweb.MethodName, webResolver)
}

r.Manager = didsubject.Manager{DB: db, MethodManagers: methodManagers}
r.Manager = didsubject.Manager{DB: db, MethodManagers: methodManagers, PreferredOrder: r.config.DIDMethods}

// Initiate the routines for auto-updating the data.
return r.networkAmbassador.Configure()
Expand Down
Loading