From 56ba974dec29b0b54ad0cd7402c4e443c3fcb937 Mon Sep 17 00:00:00 2001 From: brent s Date: Sun, 12 Dec 2021 02:29:29 -0500 Subject: [PATCH] minor fixes/improvements... --- .ref/URLs | 1 + collection_funcs.go | 193 +++++++++++++++++++++------------------ collection_funcs_test.go | 158 ++++++++++++++++++++++++++++++++ conts_test.go | 22 +++-- service_funcs.go | 22 ++++- service_funcs_test.go | 118 +++++++++++++++++++++--- 6 files changed, 405 insertions(+), 109 deletions(-) create mode 100644 collection_funcs_test.go diff --git a/.ref/URLs b/.ref/URLs index 8ef6d77..3118ce0 100644 --- a/.ref/URLs +++ b/.ref/URLs @@ -3,6 +3,7 @@ https://developer-old.gnome.org/libsecret/unstable/ https://developer-old.gnome.org/libsecret/0.18/ https://people.gnome.org/~stefw/libsecret-docs/index.html +https://freedesktop.org/wiki/Specifications/secret-storage-spec/secrets-api-0.1.html#eggdbus-interface-org.freedesktop.Secrets.Collection Quick reference URLs: diff --git a/collection_funcs.go b/collection_funcs.go index b7c18d6..5cece23 100644 --- a/collection_funcs.go +++ b/collection_funcs.go @@ -41,6 +41,66 @@ func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, er return } +// CreateItem returns a pointer to an Item based on a label, some attributes, a Secret, and whether any existing secret with the same label should be replaced or not. +func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool) (item *Item, err error) { + + var prompt *Prompt + var path dbus.ObjectPath + var promptPath dbus.ObjectPath + var variant *dbus.Variant + var props map[string]dbus.Variant = make(map[string]dbus.Variant) + + props[DbusItemLabel] = dbus.MakeVariant(label) + props[DbusItemAttributes] = dbus.MakeVariant(attrs) + + if err = c.Dbus.Call( + DbusCollectionCreateItem, 0, props, secret, replace, + ).Store(&path, &promptPath); err != nil { + return + } + + if isPrompt(promptPath) { + prompt = NewPrompt(c.Conn, promptPath) + + if variant, err = prompt.Prompt(); err != nil { + return + } + + path = variant.Value().(dbus.ObjectPath) + } + + item, err = NewItem(c, path) + + return +} + +/* + Delete removes a Collection. + While *technically* not necessary, it is recommended that you iterate through + Collection.Items and do an Item.Delete for each item *before* calling Collection.Delete; + the item paths are cached as "orphaned paths" in Dbus otherwise if not deleted before deleting + their Collection. They should clear on a reboot or restart of Dbus (but rebooting Dbus on a system in use is... troublesome). +*/ +func (c *Collection) Delete() (err error) { + + var promptPath dbus.ObjectPath + var prompt *Prompt + + if err = c.Dbus.Call(DbusCollectionDelete, 0).Store(&promptPath); err != nil { + return + } + + if isPrompt(promptPath) { + + prompt = NewPrompt(c.Conn, promptPath) + if _, err = prompt.Prompt(); err != nil { + return + } + } + + return +} + // Items returns a slice of Item pointers in the Collection. func (c *Collection) Items() (items []*Item, err error) { @@ -70,22 +130,46 @@ func (c *Collection) Items() (items []*Item, err error) { return } -// Delete removes a Collection. -func (c *Collection) Delete() (err error) { +// Label returns the Collection label (name). +func (c *Collection) Label() (label string, err error) { - var promptPath dbus.ObjectPath - var prompt *Prompt + var variant dbus.Variant - if err = c.Dbus.Call(DbusCollectionDelete, 0).Store(&promptPath); err != nil { + if variant, err = c.Dbus.GetProperty(DbusCollectionLabel); err != nil { return } - if isPrompt(promptPath) { + label = variant.Value().(string) - prompt = NewPrompt(c.Conn, promptPath) - if _, err = prompt.Prompt(); err != nil { - return - } + if label != c.name { + c.name = label + } + + return +} + +// Locked indicates if a Collection is locked (true) or unlocked (false). +func (c *Collection) Locked() (isLocked bool, err error) { + + var variant dbus.Variant + + if variant, err = c.Dbus.GetProperty(DbusCollectionLocked); err != nil { + isLocked = true + return + } + + isLocked = variant.Value().(bool) + + return +} + +// Relabel modifies the Collection's label in Dbus. +func (c *Collection) Relabel(newLabel string) (err error) { + + var variant dbus.Variant = dbus.MakeVariant(newLabel) + + if err = c.Dbus.SetProperty(DbusCollectionLabel, variant); err != nil { + return } return @@ -124,84 +208,6 @@ func (c *Collection) SearchItems(profile string) (items []*Item, err error) { return } -// CreateItem returns a pointer to an Item based on a label, some attributes, a Secret, and whether any existing secret with the same label should be replaced or not. -func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool) (item *Item, err error) { - - var prompt *Prompt - var path dbus.ObjectPath - var promptPath dbus.ObjectPath - var variant *dbus.Variant - var props map[string]dbus.Variant = make(map[string]dbus.Variant) - - props[DbusItemLabel] = dbus.MakeVariant(label) - props[DbusItemAttributes] = dbus.MakeVariant(attrs) - - if err = c.Dbus.Call( - DbusCollectionCreateItem, 0, props, secret, replace, - ).Store(&path, &promptPath); err != nil { - return - } - - if isPrompt(promptPath) { - prompt = NewPrompt(c.Conn, promptPath) - - if variant, err = prompt.Prompt(); err != nil { - return - } - - path = variant.Value().(dbus.ObjectPath) - } - - item, err = NewItem(c, path) - - return -} - -// Locked indicates if a Collection is locked (true) or unlocked (false). -func (c *Collection) Locked() (isLocked bool, err error) { - - var variant dbus.Variant - - if variant, err = c.Dbus.GetProperty(DbusCollectionLocked); err != nil { - isLocked = true - return - } - - isLocked = variant.Value().(bool) - - return -} - -// Label returns the Collection label (name). -func (c *Collection) Label() (label string, err error) { - - var variant dbus.Variant - - if variant, err = c.Dbus.GetProperty(DbusCollectionLabel); err != nil { - return - } - - label = variant.Value().(string) - - if label != c.name { - c.name = label - } - - return -} - -// Relabel modifies the Collection's label in Dbus. -func (c *Collection) Relabel(newLabel string) (err error) { - - var variant dbus.Variant = dbus.MakeVariant(newLabel) - - if err = c.Dbus.SetProperty(DbusItemLabel, variant); err != nil { - return - } - - return -} - // Created returns the time.Time of when a Collection was created. func (c *Collection) Created() (created time.Time, err error) { @@ -251,3 +257,14 @@ func (c *Collection) Modified() (modified time.Time, isChanged bool, err error) return } + +/* + setModify updates the Collection's modification time (as specified by Collection.Modified). + It seems that this does not update automatically. +*/ +func (c *Collection) setModify() (err error) { + + err = c.Dbus.SetProperty(DbusCollectionModified, uint64(time.Now().Unix())) + + return +} diff --git a/collection_funcs_test.go b/collection_funcs_test.go new file mode 100644 index 0000000..d54ff56 --- /dev/null +++ b/collection_funcs_test.go @@ -0,0 +1,158 @@ +package gosecret + +import ( + `testing` + + `github.com/godbus/dbus/v5` +) + +// Some functions are covered in the Service tests. + +/* + TestNewCollection tests the following internal functions/methods via nested calls: + + (all calls in TestNewService) + NewService + NewCollection + +*/ +func TestNewCollection(t *testing.T) { + + var svc *Service + var collection *Collection + var err error + + if svc, err = NewService(); err != nil { + t.Fatalf("NewService failed: %v", err.Error()) + } + + if collection, err = NewCollection(svc, dbus.ObjectPath(dbusDefaultCollectionPath)); err != nil { + t.Errorf( + "TestNewCollection failed when fetching collection at '%v': %v", + dbusDefaultCollectionPath, err.Error(), + ) + } + _ = collection + + if err = svc.Close(); err != nil { + t.Errorf("could not close Service.Session: %v", err.Error()) + } +} + +/* + TestCollection_Label tests the following internal functions/methods via nested calls: + (all calls in TestNewCollection) + Service.GetCollection + Collection.Label + +*/ +func TestCollection_Label(t *testing.T) { + + var svc *Service + var collection *Collection + var collLabel string + var err error + + if svc, err = NewService(); err != nil { + t.Fatalf("NewService failed: %v", err.Error()) + } + + if collection, err = svc.GetCollection(defaultCollectionLabel); err != nil { + t.Errorf( + "failed when fetching collection '%v': %v", + defaultCollectionLabel, err.Error(), + ) + err = nil + if err = svc.Close(); err != nil { + t.Errorf("could not close Service.Session: %v", err.Error()) + } + } + + if collLabel, err = collection.Label(); err != nil { + t.Errorf("cannot fetch label for '%v': %v", string(collection.Dbus.Path()), err.Error()) + if err = svc.Close(); err != nil { + t.Fatalf("could not close Service.Session: %v", err.Error()) + } + } + + if defaultCollection != collLabel { + t.Errorf("fetched collection ('%v') does not match fetched collection label ('%v')", collLabel, defaultCollection) + } + + if err = svc.Close(); err != nil { + t.Errorf("could not close Service.Session: %v", err.Error()) + } + +} + +/* + TestCollection_Items tests the following internal functions/methods via nested calls: + + (all calls in TestNewCollection) + Service.GetCollection + Collection.Items + NewSecret + Collection.CreateItem + Collection.SearchItems + Item.Delete + +*/ +func TestCollection_Items(t *testing.T) { + + var svc *Service + var collection *Collection + var items []*Item + var item *Item + var searchItemResults []*Item + var secret *Secret + var err error + + if svc, err = NewService(); err != nil { + t.Fatalf("NewService failed: %v", err.Error()) + } + + if collection, err = svc.GetCollection(defaultCollection); err != nil { + if err = svc.Close(); err != nil { + t.Errorf("could not close Service.Session: %v", err.Error()) + } + t.Fatalf("failed when fetching collection '%v': %v", + defaultCollection, err.Error(), + ) + } + + if items, err = collection.Items(); err != nil { + t.Errorf( + "failed fetching items for '%v' at '%v': %v", + defaultCollection, string(collection.Dbus.Path()), err.Error(), + ) + } else { + t.Logf("found %v items in collection '%v' at '%v'", len(items), defaultCollection, string(collection.Dbus.Path())) + } + + secret = NewSecret(svc.Session, []byte{}, []byte(testSecretContent), "text/plain") + + if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, false); err != nil { + t.Errorf( + "could not create item '%v' in collection '%v': %v", + testItemLabel, defaultCollection, err.Error(), + ) + } else { + + if searchItemResults, err = collection.SearchItems(testItemLabel); err != nil { + t.Errorf("failed to find item '%v' via Collection.SearchItems: %v", string(item.Dbus.Path()), err.Error()) + } else if len(searchItemResults) == 0 { + t.Errorf("failed to find item '%v' via Collection.SearchItems, returned 0 results (should be at least 1)", testItemLabel) + } else { + t.Logf("found %v results for Collection.SearchItems", len(searchItemResults)) + } + + if err = item.Delete(); err != nil { + t.Errorf("failed to delete created item '%v': %v", string(item.Dbus.Path()), err.Error()) + } + + } + + if err = svc.Close(); err != nil { + t.Errorf("could not close Service.Session: %v", err.Error()) + } +} diff --git a/conts_test.go b/conts_test.go index 307bb78..fbf1ab5 100644 --- a/conts_test.go +++ b/conts_test.go @@ -6,19 +6,27 @@ import ( // Paths. const ( - DbusDefaultCollectionPath string = DbusPath + "/collections/login" + dbusCollectionPath string = DbusPath + "/collection" + dbusDefaultCollectionPath string = dbusCollectionPath + "/login" ) // Strings. const ( - defaultCollection string = "default" // SHOULD point to a collection named "login"; "default" is the alias. - testAlias string = "GOSECRET_TESTING_ALIAS" - testSecretContent string = "This is a test secret for gosecret." - testItemLabel string = "gosecret_test_item" + defaultCollectionAlias string = "default" // SHOULD point to a collection named "login" (below); "default" is the alias. + defaultCollection string = "login" + defaultCollectionLabel string = "Login" // a display name; the label is lowercased and normalized for the path (per above). + testAlias string = "GOSECRET_TESTING_ALIAS" + testSecretContent string = "This is a test secret for gosecret." + testItemLabel string = "Gosecret Test Item" ) // Objects. var ( - collectionName uuid.UUID = uuid.New() - collectionAlias uuid.UUID = uuid.New() + collectionName uuid.UUID = uuid.New() + collectionAlias uuid.UUID = uuid.New() + itemAttrs map[string]string = map[string]string{ + "GOSECRET": "yes", + "foo": "bar", + "profile": testItemLabel, + } ) diff --git a/service_funcs.go b/service_funcs.go index a341eee..001d8a4 100644 --- a/service_funcs.go +++ b/service_funcs.go @@ -124,7 +124,9 @@ func (s *Service) CreateCollection(label string) (collection *Collection, err er */ func (s *Service) GetCollection(name string) (c *Collection, err error) { + var errs []error var colls []*Collection + var collLabel string // First check for an alias. if c, err = s.ReadAlias(name); err != nil && err != ErrDoesNotExist { @@ -147,8 +149,26 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) { } } + // Still nothing? Try by label. + for _, i := range colls { + if collLabel, err = i.Label(); err != nil { + errs = append(errs, err) + err = nil + continue + } + if collLabel == name { + c = i + return + } + } + // Couldn't find it by the given name. - err = ErrDoesNotExist + if errs != nil || len(errs) > 0 { + errs = append([]error{ErrDoesNotExist}, errs...) + err = NewErrors(errs...) + } else { + err = ErrDoesNotExist + } return } diff --git a/service_funcs_test.go b/service_funcs_test.go index 92082cd..ad22514 100644 --- a/service_funcs_test.go +++ b/service_funcs_test.go @@ -2,6 +2,7 @@ package gosecret import ( "testing" + `time` `github.com/godbus/dbus/v5` ) @@ -43,12 +44,16 @@ func TestNewService(t *testing.T) { NewCollection Collection.Modified NewErrors + Collection.Created */ func TestService_Collections(t *testing.T) { var err error var svc *Service var colls []*Collection + var collLabel string + var created time.Time + var modified time.Time if svc, err = NewService(); err != nil { t.Fatalf("could not get new Service via NewService: %v", err.Error()) @@ -59,6 +64,30 @@ func TestService_Collections(t *testing.T) { } else { t.Logf("found %v collections via Service.Collections", len(colls)) } + for idx, c := range colls { + if collLabel, err = c.Label(); err != nil { + t.Errorf( + "failed to get label for collection '%v': %v", + string(c.Dbus.Path()), err.Error(), + ) + } + if created, err = c.Created(); err != nil { + t.Errorf( + "failed to get created time for collection '%v': %v", + string(c.Dbus.Path()), err.Error(), + ) + } + if modified, _, err = c.Modified(); err != nil { + t.Errorf( + "failed to get modified time for collection '%v': %v", + string(c.Dbus.Path()), err.Error(), + ) + } + t.Logf( + "collection #%v (name '%v', label '%v'): created %v, last modified %v", + idx, c.name, collLabel, created, modified, + ) + } if err = svc.Close(); err != nil { t.Errorf("could not close Service.Session: %v", err.Error()) @@ -79,7 +108,7 @@ func TestService_Collections(t *testing.T) { (By extension, Service.CreateCollection is also tested as it's a very thin wrapper around Service.CreateAliasedCollection). */ -/* DISABLED. Currently, *only* the alias "default" is allowed. TODO: revisit in future? +/* DISABLED. Currently (as of 0.20.4), *only* the alias "default" is allowed. TODO: revisit in future? func TestService_CreateAliasedCollection(t *testing.T) { var err error @@ -121,12 +150,18 @@ func TestService_CreateAliasedCollection(t *testing.T) { TestService_GetCollection tests the following internal functions/methods via nested calls: (all calls in TestService_CreateAliasedCollection) + (all calls in TestService_Collections) + + + NewErrors + Collection.Created Service.GetCollection - (all calls in TestService_Collections) + NewCollection + Collection.Modified Service.ReadAlias The default collection (login) is fetched instead of creating one as this collection should exist, - and tests fetching existing collections instead of newly-created ones. + and thus this function tests fetching existing collections instead of newly-created ones. */ func TestService_GetCollection(t *testing.T) { @@ -162,6 +197,7 @@ func TestService_GetCollection(t *testing.T) { NewSecret Collection.CreateItem Item.Label + Item.Delete */ func TestService_Secrets(t *testing.T) { @@ -177,30 +213,49 @@ func TestService_Secrets(t *testing.T) { var testSecret *Secret var secretsResult map[dbus.ObjectPath]*Secret var itemPaths []dbus.ObjectPath - var itemAttrs map[string]string = map[string]string{ - "GOSECRET": "yes", - } + var created time.Time + var modified time.Time + var newModified time.Time + var wasModified bool + var isModified bool if svc, err = NewService(); err != nil { t.Fatalf("could not get new Service via NewService: %v", err.Error()) } if collection, err = svc.CreateCollection(collectionName.String()); err != nil { + t.Errorf("could not create collection '%v': %v", collectionName.String(), err.Error()) if err = svc.Close(); err != nil { - t.Errorf("could not close Service.Session: %v", err.Error()) + t.Fatalf("could not close Service.Session: %v", err.Error()) } - t.Fatalf("could not create collection '%v': %v", collectionName.String(), err.Error()) } else { t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path())) } + if created, err = collection.Created(); err != nil { + t.Errorf( + "failed to get created time for '%v': %v", + collectionName.String(), err.Error(), + ) + } + if modified, wasModified, err = collection.Modified(); err != nil { + t.Errorf( + "failed to get modified time for '%v': %v", + collectionName.String(), err.Error(), + ) + } + t.Logf( + "%v: collection '%v': created at %v, last modified at %v; was changed: %v", + time.Now(), collectionName.String(), created, modified, wasModified, + ) + // Create a secret testSecret = NewSecret(svc.Session, nil, []byte(testSecretContent), "text/plain") if testItem, err = collection.CreateItem(testItemLabel, itemAttrs, testSecret, true); err != nil { + t.Errorf("could not create Item in collection '%v': %v", collectionName.String(), err.Error()) if err = svc.Close(); err != nil { - t.Errorf("could not close Service.Session: %v", err.Error()) + t.Fatalf("could not close Service.Session: %v", err.Error()) } - t.Fatalf("could not create Item in collection '%v': %v", collectionName.String(), err.Error()) } if itemName, err = testItem.Label(); err != nil { @@ -241,7 +296,46 @@ func TestService_Secrets(t *testing.T) { } } - // Delete the collection to clean up. + // Confirm the modification information changed. + _ = newModified + _ = isModified + /* TODO: Disabled for now; it *seems* the collection modification time doesn't auto-update? See collection.setModify if not. + if newModified, isModified, err = collection.Modified(); err != nil { + t.Errorf( + "failed to get modified time for '%v': %v", + collectionName.String(), err.Error(), + ) + } + t.Logf( + "%v: (post-change) collection '%v': last modified at %v; was changed: %v", + time.Now(), collectionName.String(), newModified, isModified, + ) + if !isModified { + t.Errorf( + "modification tracking for collection '%v' failed; expected true but got false", + collectionName.String(), + ) + } else { + t.Logf("(modification check passed)") + } + if !newModified.After(modified) { + t.Errorf( + "modification timestamp update for '%v' failed: old %v, new %v", + collectionName.String(), modified, newModified, + ) + } + */ + + /* + Delete the item and collection to clean up. + *Technically* the item is deleted if the collection is, but its path is held in memory if not + manually removed, leading to a "ghost" item. + */ + if err = testItem.Delete(); err != nil { + t.Errorf("could not delete test item '%v' in collection '%v': %v", + string(testItem.Dbus.Path()), collectionName.String(), err.Error(), + ) + } if err = collection.Delete(); err != nil { t.Errorf( "error when deleting collection '%v' when testing Service: %v", @@ -254,8 +348,6 @@ func TestService_Secrets(t *testing.T) { } } -// service.Lock & service.Unlock - /* TestService_Locking tests the following internal functions/methods via nested calls: