From eb54df917869272b3ff5b8cd09d3c0aa694f86c2 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Sun, 19 Nov 2023 12:24:30 +0100 Subject: [PATCH] credential properties index --- README.rst | 8 +- discoveryservice/client.go | 91 +--------- discoveryservice/client_test.go | 16 +- discoveryservice/module.go | 8 +- discoveryservice/module_test.go | 8 +- discoveryservice/store.go | 205 +++++++++++++++++++---- discoveryservice/store_test.go | 82 +++++++-- discoveryservice/test.go | 26 +-- docs/pages/deployment/cli-reference.rst | 4 +- docs/pages/deployment/server_options.rst | 164 +++++++++--------- storage/cmd/cmd.go | 4 +- storage/engine.go | 2 +- storage/test.go | 3 +- 13 files changed, 373 insertions(+), 248 deletions(-) diff --git a/README.rst b/README.rst index 3f908d240f..d5e2702b58 100644 --- a/README.rst +++ b/README.rst @@ -176,9 +176,9 @@ The following options can be configured on the server: :widths: 20 30 50 :class: options-table - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================== + ==================================== =============================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================= Key Default Description - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================== + ==================================== =============================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================= configfile nuts.yaml Nuts config file cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. datadir ./data Directory where the node stores its files. @@ -252,12 +252,12 @@ The following options can be configured on the server: storage.redis.sentinel.password Password for authenticating to Redis Sentinels. storage.redis.sentinel.username Username for authenticating to Redis Sentinels. storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis 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 + storage.sql.connection Connection string for the SQL database. If not set, it defaults to a SQLite database stored inside the configured data directory. If specifying a SQLite database, make sure to enable foreign keys with the '_foreign_keys=on' option. **VCR** vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice). vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI. vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations. - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================== + ==================================== =============================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================= This table is automatically generated using the configuration flags in the core and engines. When they're changed the options table must be regenerated using the Makefile: diff --git a/discoveryservice/client.go b/discoveryservice/client.go index 568f6becef..41877c265e 100644 --- a/discoveryservice/client.go +++ b/discoveryservice/client.go @@ -53,60 +53,6 @@ package discoveryservice // definitions map[string]Definition //} // -//func (c *client) Search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) { -// propertyColumns := map[string]string{ -// "id": "cred.credential_id", -// "issuer": "cred.credential_issuer", -// "type": "cred.credential_type", -// "credentialSubject.id": "cred.credential_subject_id", -// } -// -// stmt := c.db.Model(&entry{}). -// Where("usecase_id = ?", serviceID). -// Joins("inner join usecase_client_credential cred ON cred.entry_id = usecase_client_entries.id") -// numProps := 0 -// for jsonPath, value := range query { -// if value == "*" { -// continue -// } -// // sort out wildcard mode -// var eq = "=" -// if strings.HasPrefix(value, "*") { -// value = "%" + value[1:] -// eq = "LIKE" -// } -// if strings.HasSuffix(value, "*") { -// value = value[:len(value)-1] + "%" -// eq = "LIKE" -// } -// if column := propertyColumns[jsonPath]; column != "" { -// stmt = stmt.Where(column+" "+eq+" ?", value) -// } else { -// // This property is not present as column, but indexed as key-value property. -// // Multiple (inner) joins to filter on a dynamic number of properties to filter on is not pretty, but it works -// alias := "p" + strconv.Itoa(numProps) -// numProps++ -// stmt = stmt.Joins("inner join usecase_client_credential_props "+alias+" ON "+alias+".id = cred.id AND "+alias+".key = ? AND "+alias+".value "+eq+" ?", jsonPath, value) -// } -// } -// -// var matches []entry -// if err := stmt.Find(&matches).Error; err != nil { -// return nil, err -// } -// var results []vc.VerifiablePresentation -// for _, match := range matches { -// if match.PresentationExpiration <= time.Now().Unix() { -// continue -// } -// presentation, err := vc.ParseVerifiablePresentation(match.PresentationRaw) -// if err != nil { -// return nil, fmt.Errorf("failed to parse presentation '%s': %w", match.PresentationID, err) -// } -// results = append(results, *presentation) -// } -// return results, nil -//} // //func (c *client) refreshAll() { // wg := &sync.WaitGroup{} @@ -120,10 +66,10 @@ package discoveryservice //} // //func (c *client) refreshList(definition Definition) error { -// var currentService discoveryService +// var currentService serviceRecord // if err := c.db.Find(¤tService, "usecase_id = ?", definition.ID).Error; errors.Is(err, gorm.ErrRecordNotFound) { // // First refresh of the list -// if err := c.db.Create(&discoveryService{ID: definition.ID}).Error; err != nil { +// if err := c.db.Create(&serviceRecord{ID: definition.ID}).Error; err != nil { // return err // } // } else if err != nil { @@ -221,10 +167,10 @@ package discoveryservice // } // subjectDID, err := curr.SubjectDID() // if err != nil { -// return fmt.Errorf("invalid credential subject ID for VP '%s': %w", presentation.ID, err) +// return fmt.Errorf("invalid credentialRecord subject ID for VP '%s': %w", presentation.ID, err) // } // credentialRecordID := uuid.NewString() -// cred := credential{ +// cred := credentialRecord{ // ID: credentialRecordID, // EntryID: entryID, // CredentialID: curr.ID.String(), @@ -233,16 +179,16 @@ package discoveryservice // CredentialType: credentialType, // } // if len(curr.CredentialSubject) != 1 { -// return errors.New("credential must contain exactly one subject") +// return errors.New("credentialRecord must contain exactly one subject") // } -// // Store credential properties +// // Store credentialRecord properties // keys, values := indexJSONObject(curr.CredentialSubject[0].(map[string]interface{}), nil, nil, "credentialSubject") // for i, key := range keys { // if key == "credentialSubject.id" { // // present as column, don't index // continue // } -// cred.Properties = append(cred.Properties, credentialProperty{ +// cred.Properties = append(cred.Properties, credentialPropertyRecord{ // ID: credentialRecordID, // Key: key, // Value: values[i], @@ -255,26 +201,3 @@ package discoveryservice // } // return nil //} -// -//// indexJSONObject indexes a JSON object, resulting in a slice of JSON paths and corresponding string values. -//// It only traverses JSON objects and only adds string values to the result. -//func indexJSONObject(target map[string]interface{}, jsonPaths []string, stringValues []string, currentPath string) ([]string, []string) { -// for key, value := range target { -// thisPath := currentPath -// if len(thisPath) > 0 { -// thisPath += "." -// } -// thisPath += key -// -// switch typedValue := value.(type) { -// case string: -// jsonPaths = append(jsonPaths, thisPath) -// stringValues = append(stringValues, typedValue) -// case map[string]interface{}: -// jsonPaths, stringValues = indexJSONObject(typedValue, jsonPaths, stringValues, thisPath) -// default: -// // other values (arrays, booleans, numbers, null) are not indexed -// } -// } -// return jsonPaths, stringValues -//} diff --git a/discoveryservice/client_test.go b/discoveryservice/client_test.go index 160bd7b200..5a257e3f2a 100644 --- a/discoveryservice/client_test.go +++ b/discoveryservice/client_test.go @@ -77,7 +77,7 @@ package discoveryservice // _ = storageEngine.Shutdown() // }) // -// t.Run("1 credential", func(t *testing.T) { +// t.Run("1 credentialRecord", func(t *testing.T) { // c := setupClient(t, storageEngine) // err := c.writePresentation(c.db, TestDefinition.ID, vpAlice) // require.NoError(t, err) @@ -89,7 +89,7 @@ package discoveryservice // require.Equal(t, vpAlice.Raw(), entries[0].PresentationRaw) // require.Equal(t, vpAlice.JWT().Expiration().Unix(), entries[0].PresentationExpiration) // -// var credentials []credential +// var credentials []credentialRecord // require.NoError(t, c.db.Find(&credentials, "entry_id = ?", entries[0].ID).Error) // require.Len(t, credentials, 1) // cred := credentials[0] @@ -107,7 +107,7 @@ package discoveryservice // } // for recordID, properties := range expectedProperties { // for key, value := range properties { -// var prop credentialProperty +// var prop credentialPropertyRecord // require.NoError(t, c.db.Find(&prop, "id = ? AND key = ?", recordID, key).Error) // require.Equal(t, value, prop.Value) // } @@ -282,7 +282,7 @@ package discoveryservice // require.NoError(t, err) // tables := []schema.Tabler{ // &entry{}, -// &credential{}, +// &credentialRecord{}, // &list{}, // } // for _, table := range tables { @@ -318,7 +318,7 @@ package discoveryservice // bobDID = did.MustParseDID("did:example:bob") // keyPairs[bobDID.String()], _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // -// vcAlice = createCredentialWithClaims(authorityDID, aliceDID, func() []interface{} { +// vcAlice = createCredentialCustom(authorityDID, aliceDID, func() []interface{} { // return []interface{}{ // map[string]interface{}{ // "id": aliceDID.String(), @@ -333,7 +333,7 @@ package discoveryservice // // do nothing // }) // vpAlice = createPresentation(aliceDID, vcAlice) -// vcBob = createCredentialWithClaims(authorityDID, bobDID, func() []interface{} { +// vcBob = createCredentialCustom(authorityDID, bobDID, func() []interface{} { // return []interface{}{ // map[string]interface{}{ // "id": aliceDID.String(), @@ -351,7 +351,7 @@ package discoveryservice //} // //func createCredential(issuerDID did.DID, subjectDID did.DID) vc.VerifiableCredential { -// return createCredentialWithClaims(issuerDID, subjectDID, +// return createCredentialCustom(issuerDID, subjectDID, // func() []interface{} { // return []interface{}{ // map[string]interface{}{ @@ -364,7 +364,7 @@ package discoveryservice // }) //} // -//func createCredentialWithClaims(issuerDID did.DID, subjectDID did.DID, credentialSubjectCreator func() []interface{}, claimVisitor func(map[string]interface{})) vc.VerifiableCredential { +//func createCredentialCustom(issuerDID did.DID, subjectDID did.DID, credentialSubjectCreator func() []interface{}, claimVisitor func(map[string]interface{})) vc.VerifiableCredential { // vcID := did.DIDURL{DID: issuerDID} // vcID.Fragment = uuid.NewString() // vcIDURI := vcID.URI() diff --git a/discoveryservice/module.go b/discoveryservice/module.go index 1a4852cb98..ee75158780 100644 --- a/discoveryservice/module.go +++ b/discoveryservice/module.go @@ -140,14 +140,14 @@ func (m *Module) Add(serviceID string, presentation vc.VerifiablePresentation) e func (m *Module) addPresentation(definition Definition, presentation vc.VerifiablePresentation) error { // Must contain credentials if len(presentation.VerifiableCredential) == 0 { - return errors.New("presentation must contain at least one credential") + return errors.New("presentation must contain at least one credentialRecord") } - // VP can't be valid longer than the credential it contains + // VP can't be valid longer than the credentialRecord it contains expiration := presentation.JWT().Expiration() for _, cred := range presentation.VerifiableCredential { exp := cred.JWT().Expiration() if !exp.IsZero() && expiration.After(exp) { - return fmt.Errorf("presentation is valid longer than the credential(s) it contains") + return fmt.Errorf("presentation is valid longer than the credentialRecord(s) it contains") } } // VP must fulfill the PEX Presentation Definition @@ -159,7 +159,7 @@ func (m *Module) addPresentation(definition Definition, presentation vc.Verifiab } func (m *Module) addRetraction(serviceID string, presentation vc.VerifiablePresentation) error { - // Presentation might be a retraction (deletion of an earlier credential) must contain no credentials, and refer to the VP being retracted by ID. + // Presentation might be a retraction (deletion of an earlier credentialRecord) must contain no credentials, and refer to the VP being retracted by ID. // If those conditions aren't met, we don't need to register the retraction. if len(presentation.VerifiableCredential) > 0 { return errors.New("retraction presentation must not contain credentials") diff --git a/discoveryservice/module_test.go b/discoveryservice/module_test.go index a978da620a..9a9bb388ed 100644 --- a/discoveryservice/module_test.go +++ b/discoveryservice/module_test.go @@ -51,7 +51,7 @@ func Test_Module_Add(t *testing.T) { require.NoError(t, err) assert.Equal(t, Timestamp(1), *timestamp) }) - t.Run("replace presentation of same credential subject", func(t *testing.T) { + t.Run("replace presentation of same credentialRecord subject", func(t *testing.T) { m := setupModule(t, storageEngine) vpAlice2 := createPresentation(aliceDID, vcAlice) @@ -84,12 +84,12 @@ func Test_Module_Add(t *testing.T) { t.Run("valid longer than its credentials", func(t *testing.T) { m := setupModule(t, storageEngine) - vcAlice := createCredentialWithClaims(authorityDID, aliceDID, func(claims map[string]interface{}) { + vcAlice := createCredentialCustom(authorityDID, aliceDID, nil, func(claims map[string]interface{}) { claims["exp"] = time.Now().Add(time.Hour) }) vpAlice := createPresentation(aliceDID, vcAlice) err := m.Add(testServiceID, vpAlice) - assert.EqualError(t, err, "presentation is valid longer than the credential(s) it contains") + assert.EqualError(t, err, "presentation is valid longer than the credentialRecord(s) it contains") }) t.Run("not valid long enough", func(t *testing.T) { m := setupModule(t, storageEngine) @@ -237,7 +237,7 @@ func Test_Module_Get(t *testing.T) { } func setupModule(t *testing.T, storageInstance storage.Engine) *Module { - resetStoreAfterTest(t, storageInstance.GetSQLDatabase()) + resetStore(t, storageInstance.GetSQLDatabase()) m := New(storageInstance) require.NoError(t, m.Configure(core.ServerConfig{})) m.services = testDefinitions() diff --git a/discoveryservice/store.go b/discoveryservice/store.go index dd53898e9a..5d604bf4f0 100644 --- a/discoveryservice/store.go +++ b/discoveryservice/store.go @@ -24,28 +24,30 @@ import ( "github.com/google/uuid" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/discoveryservice/log" - credential2 "github.com/nuts-foundation/nuts-node/vcr/credential" + credential "github.com/nuts-foundation/nuts-node/vcr/credential" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/schema" + "strconv" + "strings" "time" ) var ErrServiceNotFound = errors.New("discovery service not found") var ErrPresentationAlreadyExists = errors.New("presentation already exists") -type discoveryService struct { +type serviceRecord struct { ID string `gorm:"primaryKey"` Timestamp uint64 } -func (s discoveryService) TableName() string { +func (s serviceRecord) TableName() string { return "discoveryservices" } -var _ schema.Tabler = (*servicePresentation)(nil) +var _ schema.Tabler = (*presentationRecord)(nil) -type servicePresentation struct { +type presentationRecord struct { ID string `gorm:"primaryKey"` ServiceID string Timestamp uint64 @@ -53,18 +55,18 @@ type servicePresentation struct { PresentationID string PresentationRaw string PresentationExpiration int64 - Credentials []credential `gorm:"foreignKey:PresentationID;references:ID"` + Credentials []credentialRecord `gorm:"foreignKey:PresentationID;references:ID"` } -func (s servicePresentation) TableName() string { +func (s presentationRecord) TableName() string { return "discoveryservice_presentations" } -// credential is a Verifiable Credential, part of a presentation (entry) on a use case list. -type credential struct { +// credentialRecord is a Verifiable Credential, part of a presentation (entry) on a use case list. +type credentialRecord struct { // ID is the unique identifier of the entry. ID string `gorm:"primaryKey"` - // PresentationID corresponds to the discoveryservice_presentations record ID (not VerifiablePresentation.ID) this credential belongs to. + // PresentationID corresponds to the discoveryservice_presentations record ID (not VerifiablePresentation.ID) this credentialRecord belongs to. PresentationID string // CredentialID contains the 'id' property of the Verifiable Credential. CredentialID string @@ -74,16 +76,16 @@ type credential struct { CredentialSubjectID string // CredentialType contains the 'type' property of the Verifiable Credential (not being 'VerifiableCredential'). CredentialType *string - Properties []credentialProperty `gorm:"foreignKey:ID;references:ID"` + Properties []credentialPropertyRecord `gorm:"foreignKey:ID;references:ID"` } // TableName returns the table name for this DTO. -func (p credential) TableName() string { +func (p credentialRecord) TableName() string { return "discoveryservice_credentials" } -// credentialProperty is a property of a Verifiable Credential in a Verifiable Presentation in a discovery service. -type credentialProperty struct { +// credentialPropertyRecord is a property of a Verifiable Credential in a Verifiable Presentation in a discovery service. +type credentialPropertyRecord struct { // ID refers to the entry record in discoveryservice_credentials ID string `gorm:"primaryKey"` // Key is JSON path of the property. @@ -93,7 +95,7 @@ type credentialProperty struct { } // TableName returns the table name for this DTO. -func (l credentialProperty) TableName() string { +func (l credentialPropertyRecord) TableName() string { return "discoveryservice_credential_props" } @@ -104,7 +106,7 @@ type sqlStore struct { func newSQLStore(db *gorm.DB, definitions map[string]Definition) (*sqlStore, error) { // Creates entries in the discovery service table with initial timestamp, if they don't exist yet for _, definition := range definitions { - currentList := discoveryService{ + currentList := serviceRecord{ ID: definition.ID, } if err := db.FirstOrCreate(¤tList, "id = ?", definition.ID).Error; err != nil { @@ -121,7 +123,7 @@ func newSQLStore(db *gorm.DB, definitions map[string]Definition) (*sqlStore, err // If the local node is the Discovery Server and thus is responsible for the timestamping, // nil should be passed to let the store determine the right value. func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, timestamp *Timestamp) error { - credentialSubjectID, err := credential2.PresentationSigner(presentation) + credentialSubjectID, err := credential.PresentationSigner(presentation) if err != nil { return err } @@ -135,30 +137,87 @@ func (s *sqlStore) add(serviceID string, presentation vc.VerifiablePresentation, } return s.db.Transaction(func(tx *gorm.DB) error { - timestamp, err := s.updateTimestamp(tx, serviceID, timestamp) + newTimestamp, err := s.updateTimestamp(tx, serviceID, timestamp) if err != nil { return err } // Delete any previous presentations of the subject - if err := tx.Delete(&servicePresentation{}, "service_id = ? AND credential_subject_id = ?", serviceID, credentialSubjectID.String()). + if err := tx.Delete(&presentationRecord{}, "service_id = ? AND credential_subject_id = ?", serviceID, credentialSubjectID.String()). Error; err != nil { return err } - // Now store the presentation itself - return tx.Create(&servicePresentation{ - ID: uuid.NewString(), - ServiceID: serviceID, - Timestamp: uint64(timestamp), - CredentialSubjectID: credentialSubjectID.String(), - PresentationID: presentation.ID.String(), - PresentationRaw: presentation.Raw(), - PresentationExpiration: presentation.JWT().Expiration().Unix(), - }).Error + + newPresentation, err := createPresentationRecord(serviceID, newTimestamp, presentation) + if err != nil { + return err + } + + return tx.Create(&newPresentation).Error }) } +// createPresentationRecord creates a presentationRecord from a VerifiablePresentation. +// It creates the following types: +// - presentationRecord +// - presentationRecord.Credentials with credentialRecords of the credentials in the presentation +// - presentationRecord.Credentials.Properties of the credentialSubject properties of the credential (for s +func createPresentationRecord(serviceID string, timestamp Timestamp, presentation vc.VerifiablePresentation) (*presentationRecord, error) { + credentialSubjectID, err := credential.PresentationSigner(presentation) + if err != nil { + return nil, err + } + + newPresentation := presentationRecord{ + ID: uuid.NewString(), + ServiceID: serviceID, + Timestamp: uint64(timestamp), + CredentialSubjectID: credentialSubjectID.String(), + PresentationID: presentation.ID.String(), + PresentationRaw: presentation.Raw(), + PresentationExpiration: presentation.JWT().Expiration().Unix(), + } + + for _, currCred := range presentation.VerifiableCredential { + var credentialType *string + for _, currType := range currCred.Type { + if currType.String() != "VerifiableCredential" { + credentialType = new(string) + *credentialType = currType.String() + break + } + } + if len(currCred.CredentialSubject) != 1 { + return nil, errors.New("credential must contain exactly one subject") + } + + newCredential := credentialRecord{ + ID: uuid.NewString(), + PresentationID: newPresentation.ID, + CredentialID: currCred.ID.String(), + CredentialIssuer: currCred.Issuer.String(), + CredentialSubjectID: credentialSubjectID.String(), + CredentialType: credentialType, + } + // Store credential's properties + keys, values := indexJSONObject(currCred.CredentialSubject[0].(map[string]interface{}), nil, nil, "credentialSubject") + for i, key := range keys { + if key == "credentialSubject.id" { + // present as column, don't index + continue + } + newCredential.Properties = append(newCredential.Properties, credentialPropertyRecord{ + ID: newCredential.ID, + Key: key, + Value: values[i], + }) + } + newPresentation.Credentials = append(newPresentation.Credentials, newCredential) + } + return &newPresentation, nil +} + func (s *sqlStore) get(serviceID string, startAt Timestamp) ([]vc.VerifiablePresentation, *Timestamp, error) { - var rows []servicePresentation + var rows []presentationRecord err := s.db.Order("timestamp ASC").Find(&rows, "service_id = ? AND timestamp > ?", serviceID, int(startAt)).Error if err != nil { return nil, nil, fmt.Errorf("query service '%s': %w", serviceID, err) @@ -176,11 +235,66 @@ func (s *sqlStore) get(serviceID string, startAt Timestamp) ([]vc.VerifiablePres return presentations, ×tamp, nil } +func (s *sqlStore) search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) { + propertyColumns := map[string]string{ + "id": "cred.credential_id", + "issuer": "cred.credential_issuer", + "type": "cred.credential_type", + "credentialSubject.id": "cred.credential_subject_id", + } + + stmt := s.db.Model(&presentationRecord{}). + Where("service_id = ?", serviceID). + Joins("inner join discoveryservice_credentials cred ON cred.entry_id = discoveryservice_presentations.id") + numProps := 0 + for jsonPath, value := range query { + if value == "*" { + continue + } + // sort out wildcard mode + var eq = "=" + if strings.HasPrefix(value, "*") { + value = "%" + value[1:] + eq = "LIKE" + } + if strings.HasSuffix(value, "*") { + value = value[:len(value)-1] + "%" + eq = "LIKE" + } + if column := propertyColumns[jsonPath]; column != "" { + stmt = stmt.Where(column+" "+eq+" ?", value) + } else { + // This property is not present as column, but indexed as key-value property. + // Multiple (inner) joins to filter on a dynamic number of properties to filter on is not pretty, but it works + alias := "p" + strconv.Itoa(numProps) + numProps++ + stmt = stmt.Joins("inner join discoveryservice_credential_props "+alias+" ON "+alias+".id = cred.id AND "+alias+".key = ? AND "+alias+".value "+eq+" ?", jsonPath, value) + } + } + + var matches []presentationRecord + if err := stmt.Find(&matches).Error; err != nil { + return nil, err + } + var results []vc.VerifiablePresentation + for _, match := range matches { + if match.PresentationExpiration <= time.Now().Unix() { + continue + } + presentation, err := vc.ParseVerifiablePresentation(match.PresentationRaw) + if err != nil { + return nil, fmt.Errorf("failed to parse presentation '%s': %w", match.PresentationID, err) + } + results = append(results, *presentation) + } + return results, nil +} + func (s *sqlStore) updateTimestamp(tx *gorm.DB, serviceID string, newTimestamp *Timestamp) (Timestamp, error) { - var result discoveryService + var result serviceRecord // Lock (SELECT FOR UPDATE) discoveryservices row to prevent concurrent updates to the same list, which could mess up the lamport timestamp. if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). - Where(discoveryService{ID: serviceID}). + Where(serviceRecord{ID: serviceID}). Find(&result). Error; err != nil { return 0, err @@ -200,7 +314,7 @@ func (s *sqlStore) updateTimestamp(tx *gorm.DB, serviceID string, newTimestamp * func (s *sqlStore) exists(serviceID string, credentialSubjectID string, presentationID string) (bool, error) { var count int64 - if err := s.db.Model(servicePresentation{}).Where(servicePresentation{ + if err := s.db.Model(presentationRecord{}).Where(presentationRecord{ ServiceID: serviceID, CredentialSubjectID: credentialSubjectID, PresentationID: presentationID, @@ -222,9 +336,32 @@ func (s *sqlStore) prune() error { } func (s *sqlStore) removeExpired() (int, error) { - result := s.db.Where("presentation_expiration < ?", time.Now().Unix()).Delete(servicePresentation{}) + result := s.db.Where("presentation_expiration < ?", time.Now().Unix()).Delete(presentationRecord{}) if result.Error != nil { return 0, fmt.Errorf("prune presentations: %w", result.Error) } return int(result.RowsAffected), nil } + +// indexJSONObject indexes a JSON object, resulting in a slice of JSON paths and corresponding string values. +// It only traverses JSON objects and only adds string values to the result. +func indexJSONObject(target map[string]interface{}, jsonPaths []string, stringValues []string, currentPath string) ([]string, []string) { + for key, value := range target { + thisPath := currentPath + if len(thisPath) > 0 { + thisPath += "." + } + thisPath += key + + switch typedValue := value.(type) { + case string: + jsonPaths = append(jsonPaths, thisPath) + stringValues = append(stringValues, typedValue) + case map[string]interface{}: + jsonPaths, stringValues = indexJSONObject(typedValue, jsonPaths, stringValues, thisPath) + default: + // other values (arrays, booleans, numbers, null) are not indexed + } + } + return jsonPaths, stringValues +} diff --git a/discoveryservice/store_test.go b/discoveryservice/store_test.go index 1627a6d446..1c14121eba 100644 --- a/discoveryservice/store_test.go +++ b/discoveryservice/store_test.go @@ -59,20 +59,82 @@ func Test_sqlStore_exists(t *testing.T) { }) } +func Test_sqlStore_add(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + + t.Run("no credentials in presentation", func(t *testing.T) { + m := setupStore(t, storageEngine.GetSQLDatabase()) + err := m.add(testServiceID, createPresentation(aliceDID), nil) + assert.NoError(t, err) + }) + t.Run("no indexable properties in credential", func(t *testing.T) { + m := setupStore(t, storageEngine.GetSQLDatabase()) + err := m.add(testServiceID, createPresentation(aliceDID, createCredential(authorityDID, aliceDID)), nil) + assert.NoError(t, err) + + var actual []credentialPropertyRecord + assert.NoError(t, m.db.Find(&actual).Error) + assert.Empty(t, actual) + }) + t.Run("with indexable properties in credential", func(t *testing.T) { + m := setupStore(t, storageEngine.GetSQLDatabase()) + err := m.add(testServiceID, createPresentation(aliceDID, createCredentialCustom(authorityDID, aliceDID, map[string]interface{}{ + "name": "Alice", + "placeOfBirth": "Bristol", + }, nil)), nil) + assert.NoError(t, err) + + var actual []credentialPropertyRecord + assert.NoError(t, m.db.Find(&actual).Error) + require.Len(t, actual, 2) + assert.Equal(t, "Alice", sliceToMap(actual)["credentialSubject.name"]) + assert.Equal(t, "Bristol", sliceToMap(actual)["credentialSubject.placeOfBirth"]) + }) + t.Run("with non-indexable properties in credential", func(t *testing.T) { + m := setupStore(t, storageEngine.GetSQLDatabase()) + err := m.add(testServiceID, createPresentation(aliceDID, createCredentialCustom(authorityDID, aliceDID, map[string]interface{}{ + "name": "Alice", + "age": 35, + }, nil)), nil) + assert.NoError(t, err) + + var actual []credentialPropertyRecord + assert.NoError(t, m.db.Find(&actual).Error) + require.Len(t, actual, 1) + assert.Equal(t, "Alice", sliceToMap(actual)["credentialSubject.name"]) + }) + t.Run("without indexable properties in credential", func(t *testing.T) { + m := setupStore(t, storageEngine.GetSQLDatabase()) + presentation := createCredential(authorityDID, aliceDID) + err := m.add(testServiceID, createPresentation(aliceDID, presentation), nil) + assert.NoError(t, err) + + var actual []credentialPropertyRecord + assert.NoError(t, m.db.Find(&actual).Error) + assert.Empty(t, actual) + }) +} + func setupStore(t *testing.T, db *gorm.DB) *sqlStore { - resetStoreAfterTest(t, db) + resetStore(t, db) store, err := newSQLStore(db, testDefinitions()) require.NoError(t, err) return store } -func resetStoreAfterTest(t *testing.T, db *gorm.DB) { - t.Cleanup(func() { - underlyingDB, err := db.DB() - require.NoError(t, err) - _, err = underlyingDB.Exec("DELETE FROM discoveryservice_presentations") - require.NoError(t, err) - _, err = underlyingDB.Exec("DELETE FROM discoveryservices") - require.NoError(t, err) - }) +func resetStore(t *testing.T, db *gorm.DB) { + underlyingDB, err := db.DB() + require.NoError(t, err) + // related tables are emptied due to on-delete-cascade clause + _, err = underlyingDB.Exec("DELETE FROM discoveryservices") + require.NoError(t, err) +} + +func sliceToMap(slice []credentialPropertyRecord) map[string]string { + var result = make(map[string]string) + for _, curr := range slice { + result[curr.Key] = curr.Value + } + return result } diff --git a/discoveryservice/test.go b/discoveryservice/test.go index 43f74082f8..edecb4be85 100644 --- a/discoveryservice/test.go +++ b/discoveryservice/test.go @@ -110,28 +110,30 @@ func init() { } func createCredential(issuerDID did.DID, subjectDID did.DID) vc.VerifiableCredential { - return createCredentialWithClaims(issuerDID, subjectDID, func(claims map[string]interface{}) { + return createCredentialCustom(issuerDID, subjectDID, nil, func(claims map[string]interface{}) { // do nothing }) } -func createCredentialWithClaims(issuerDID did.DID, subjectDID did.DID, claimVisitor func(map[string]interface{})) vc.VerifiableCredential { +func createCredentialCustom(issuerDID did.DID, subjectDID did.DID, credentialSubject map[string]interface{}, claimVisitor func(map[string]interface{})) vc.VerifiableCredential { vcID := did.DIDURL{DID: issuerDID} vcID.Fragment = uuid.NewString() vcIDURI := vcID.URI() expirationDate := time.Now().Add(time.Hour * 24) + if credentialSubject == nil { + credentialSubject = make(map[string]interface{}) + } + credentialSubject["id"] = subjectDID.String() result, err := vc.CreateJWTVerifiableCredential(context.Background(), vc.VerifiableCredential{ - ID: &vcIDURI, - Issuer: issuerDID.URI(), - IssuanceDate: time.Now(), - ExpirationDate: &expirationDate, - CredentialSubject: []interface{}{ - map[string]interface{}{ - "id": subjectDID.String(), - }, - }, + ID: &vcIDURI, + Issuer: issuerDID.URI(), + IssuanceDate: time.Now(), + ExpirationDate: &expirationDate, + CredentialSubject: []interface{}{credentialSubject}, }, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { - claimVisitor(claims) + if claimVisitor != nil { + claimVisitor(claims) + } return signJWT(subjectDID, claims, headers) }) if err != nil { diff --git a/docs/pages/deployment/cli-reference.rst b/docs/pages/deployment/cli-reference.rst index ca69be9aee..abd69714d5 100755 --- a/docs/pages/deployment/cli-reference.rst +++ b/docs/pages/deployment/cli-reference.rst @@ -44,7 +44,7 @@ The following options apply to the server commands below: --http.default.log string What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). (default "metadata") --http.default.tls string Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', --internalratelimiter When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. (default true) - --jsonld.contexts.localmapping stringToString This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. (default [https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson]) + --jsonld.contexts.localmapping stringToString This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. (default [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson]) --jsonld.contexts.remoteallowlist strings In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. (default [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json]) --loggerformat string Log format (text, json) (default "text") --network.bootstrapnodes strings List of bootstrap nodes (':') which the node initially connect to. @@ -70,7 +70,7 @@ The following options apply to the server commands below: --storage.redis.sentinel.username string Username for authenticating to Redis Sentinels. --storage.redis.tls.truststorefile string PEM file containing the trusted CA certificate(s) for authenticating remote Redis servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). --storage.redis.username string Redis database username. If set, it overrides the username in the connection URL. - --storage.sql.connection string Connection string for the SQL database. If not set, it defaults to a SQLite database stored inside the configured data directory + --storage.sql.connection string Connection string for the SQL database. If not set, it defaults to a SQLite database stored inside the configured data directory. If specifying a SQLite database, make sure to enable foreign keys with the '_foreign_keys=on' option. --strictmode When set, insecure settings are forbidden. (default true) --tls.certfile string PEM file containing the certificate for the server (also used as client certificate). --tls.certheader string Name of the HTTP header that will contain the client certificate when TLS is offloaded. diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index fbe48394e1..c20776dd0b 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -2,85 +2,85 @@ :widths: 20 30 50 :class: options-table - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================== - Key Default Description - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================== - configfile nuts.yaml Nuts config file - cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. - datadir ./data Directory where the node stores its files. - internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. - loggerformat text Log format (text, json) - strictmode true When set, insecure settings are forbidden. - verbosity info Log level (trace, debug, info, warn, error) - tls.certfile PEM file containing the certificate for the server (also used as client certificate). - tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. - tls.certkeyfile PEM file containing the private key of the server certificate. - tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. - tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. - **Auth** - auth.accesstokenlifespan 60 defines how long (in seconds) an access token is valid. Uses default in strict mode. - auth.clockskew 5000 allowed JWT Clock skew in milliseconds - auth.contractvalidators [irma,uzi,dummy,employeeid] sets the different contract validators to use - auth.publicurl public URL which can be reached by a users IRMA client, this should include the scheme and domain: https://example.com. Additional paths should only be added if some sort of url-rewriting is done in a reverse-proxy. - auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client - auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. - auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. - **Crypto** - crypto.storage fs Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). - crypto.external.address Address of the external storage service. - crypto.external.timeout 100ms Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). - crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. - crypto.vault.pathprefix kv The Vault path prefix. - crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). - crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. - **Events** - events.nats.hostname 0.0.0.0 Hostname for the NATS server - events.nats.port 4222 Port where the NATS server listens on - events.nats.storagedir Directory where file-backed streams are stored in the NATS server - events.nats.timeout 30 Timeout for NATS server operations - **GoldenHammer** - goldenhammer.enabled true Whether to enable automatically fixing DID documents with the required endpoints. - goldenhammer.interval 10m0s The interval in which to check for DID documents to fix. - **HTTP** - http.default.address \:1323 Address and port the server will be listening to - http.default.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). - http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', - http.default.auth.audience Expected audience for JWT tokens (default: hostname) - http.default.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers - http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. - http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. - **JSONLD** - jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. - jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. - **Network** - network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. - network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). - network.enablediscovery true Whether to enable automatic connecting to other nodes. - network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. - network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). - network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). - network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. - network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. - network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). - network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. - **PKI** - pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail - pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true - **Storage** - storage.bbolt.backup.directory Target directory for BBolt database backups. - storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. - storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. - storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. - storage.redis.password Redis database password. If set, it overrides the username in the connection URL. - storage.redis.username Redis database username. If set, it overrides the username in the connection URL. - storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. - storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. - storage.redis.sentinel.password Password for authenticating to Redis Sentinels. - storage.redis.sentinel.username Username for authenticating to Redis Sentinels. - storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis 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 - **VCR** - vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice). - vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI. - vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations. - ==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================== + ==================================== =============================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================= + Key Default Description + ==================================== =============================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================= + configfile nuts.yaml Nuts config file + cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. + datadir ./data Directory where the node stores its files. + internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. + loggerformat text Log format (text, json) + strictmode true When set, insecure settings are forbidden. + verbosity info Log level (trace, debug, info, warn, error) + tls.certfile PEM file containing the certificate for the server (also used as client certificate). + tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. + tls.certkeyfile PEM file containing the private key of the server certificate. + tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. + tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. + **Auth** + auth.accesstokenlifespan 60 defines how long (in seconds) an access token is valid. Uses default in strict mode. + auth.clockskew 5000 allowed JWT Clock skew in milliseconds + auth.contractvalidators [irma,uzi,dummy,employeeid] sets the different contract validators to use + auth.publicurl public URL which can be reached by a users IRMA client, this should include the scheme and domain: https://example.com. Additional paths should only be added if some sort of url-rewriting is done in a reverse-proxy. + auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client + auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. + auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. + **Crypto** + crypto.storage fs Storage to use, 'external' for an external backend (experimental), 'fs' for file system (for development purposes), 'vaultkv' for Vault KV store (recommended, will be replaced by external backend in future). + crypto.external.address Address of the external storage service. + crypto.external.timeout 100ms Time-out when invoking the external storage backend, in Golang time.Duration string format (e.g. 1s). + crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. + crypto.vault.pathprefix kv The Vault path prefix. + crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 1s). + crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. + **Events** + events.nats.hostname 0.0.0.0 Hostname for the NATS server + events.nats.port 4222 Port where the NATS server listens on + events.nats.storagedir Directory where file-backed streams are stored in the NATS server + events.nats.timeout 30 Timeout for NATS server operations + **GoldenHammer** + goldenhammer.enabled true Whether to enable automatically fixing DID documents with the required endpoints. + goldenhammer.interval 10m0s The interval in which to check for DID documents to fix. + **HTTP** + http.default.address \:1323 Address and port the server will be listening to + http.default.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). + http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', + http.default.auth.audience Expected audience for JWT tokens (default: hostname) + http.default.auth.authorizedkeyspath Path to an authorized_keys file for trusted JWT signers + http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode. + http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. + **JSONLD** + jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. + **Network** + network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. + network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). + network.enablediscovery true Whether to enable automatic connecting to other nodes. + network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. + network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). + network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). + network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. + network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. + network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). + network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. + **PKI** + pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail + pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true + **Storage** + storage.bbolt.backup.directory Target directory for BBolt database backups. + storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. + storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. + storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. + storage.redis.password Redis database password. If set, it overrides the username in the connection URL. + storage.redis.username Redis database username. If set, it overrides the username in the connection URL. + storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. + storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. + storage.redis.sentinel.password Password for authenticating to Redis Sentinels. + storage.redis.sentinel.username Username for authenticating to Redis Sentinels. + storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis 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. If specifying a SQLite database, make sure to enable foreign keys with the '_foreign_keys=on' option. + **VCR** + vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice). + vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI. + vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations. + ==================================== =============================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================= diff --git a/storage/cmd/cmd.go b/storage/cmd/cmd.go index 9cf8d59626..5fe495bf2b 100644 --- a/storage/cmd/cmd.go +++ b/storage/cmd/cmd.go @@ -38,6 +38,8 @@ func FlagSet() *pflag.FlagSet { flagSet.StringSlice("storage.redis.sentinel.nodes", defs.Redis.Sentinel.Nodes, "Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel.") flagSet.String("storage.redis.sentinel.username", defs.Redis.Sentinel.Username, "Username for authenticating to Redis Sentinels.") flagSet.String("storage.redis.sentinel.password", defs.Redis.Sentinel.Password, "Password for authenticating to Redis Sentinels.") - flagSet.String("storage.sql.connection", defs.SQL.ConnectionString, "Connection string for the SQL database. If not set, it defaults to a SQLite database stored inside the configured data directory") + flagSet.String("storage.sql.connection", defs.SQL.ConnectionString, "Connection string for the SQL database. "+ + "If not set, it defaults to a SQLite database stored inside the configured data directory. "+ + "If specifying a SQLite database, make sure to enable foreign keys with the '_foreign_keys=on' query parameter.") return flagSet } diff --git a/storage/engine.go b/storage/engine.go index fecd2db7d6..10f7309960 100644 --- a/storage/engine.go +++ b/storage/engine.go @@ -162,7 +162,7 @@ func (e *engine) GetSQLDatabase() *gorm.DB { func (e *engine) initSQLDatabase() error { connectionString := e.config.SQL.ConnectionString if len(connectionString) == 0 { - connectionString = "file:" + path.Join(e.datadir, "sqlite.db") + connectionString = "file:" + path.Join(e.datadir, "sqlite.db?_foreign_keys=on") } var err error e.sqlDB, err = gorm.Open(sqlite.Open(connectionString), &gorm.Config{}) diff --git a/storage/test.go b/storage/test.go index 299fc89faa..9b5820b42a 100644 --- a/storage/test.go +++ b/storage/test.go @@ -29,12 +29,11 @@ import ( ) // SQLiteInMemoryConnectionString is a connection string for an in-memory SQLite database -const SQLiteInMemoryConnectionString = "file::memory:?cache=shared" +const SQLiteInMemoryConnectionString = "file::memory:?cache=shared&_foreign_keys=on" func NewTestStorageEngineInDir(dir string) Engine { result := New().(*engine) result.config.SQL = SQLConfig{ConnectionString: SQLiteInMemoryConnectionString} - //result.config.SQL = SQLConfig{ConnectionString: "file:../../data/sqlite.db"} _ = result.Configure(core.TestServerConfig(core.ServerConfig{Datadir: dir + "/data"})) return result }