v1 complete and tests complete (for now. 😉).

This commit is contained in:
brent s. 2021-12-13 00:15:38 -05:00
parent cf354c3fa9
commit d81452a92c
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
9 changed files with 310 additions and 58 deletions

View File

@ -99,12 +99,10 @@ Service


And so on. And so on.


In *practice*, however, most users will only have two Session types: In *practice*, however, most users will only have two ``Collection``s:


* a default "system" one, and * a default "system" one named `login` (usually unlocked upon login), and
* a temporary one that may or may not exist, running in memory for the current login session * a temporary one that may or may not exist, running in memory for the current login session named `session`

and a single Collection, named `login` (and aliased to `default`, usually).


== Usage == Usage


@ -235,3 +233,5 @@ Many functions are consolidated into a single test due to how dependent certain
Obviously since this library interacts directly with Dbus (and I don't want to spend the time to mock up an entire Dbus-like interface to test), all tests are integration tests rather than unit tests. Therefore in the event of a failed run, you will need to open e.g. Seahorse or d-feet or some other Dbus/SecretService browser and manually delete the created Secret Service collection. It/they should be easily identified; they use a generated UUID4 string as the collection name and it is highly unlikely that you will see any other collections named as such. If running `go test` with the verbose flag (`-v`), the name and path of the collection will be printed out. If all tests pass, the test collection should be removed automatically. Obviously since this library interacts directly with Dbus (and I don't want to spend the time to mock up an entire Dbus-like interface to test), all tests are integration tests rather than unit tests. Therefore in the event of a failed run, you will need to open e.g. Seahorse or d-feet or some other Dbus/SecretService browser and manually delete the created Secret Service collection. It/they should be easily identified; they use a generated UUID4 string as the collection name and it is highly unlikely that you will see any other collections named as such. If running `go test` with the verbose flag (`-v`), the name and path of the collection will be printed out. If all tests pass, the test collection should be removed automatically.


The same UUID is used for all tests in a test run. The same UUID is used for all tests in a test run.

You may be prompted during a test run for a password; you can simply use a blank password for this as it is the password used to protect a collection. This prompt pops up during the creation of a Collection.

View File

@ -41,16 +41,30 @@ func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, er
return 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) { CreateItem returns a pointer to an Item based on a label, some attributes, a Secret,
whether any existing secret with the same label should be replaced or not, and the optional itemType.

itemType is optional; if specified, it should be a Dbus interface (only the first element is used).
If not specified, the default DbusDefaultItemType will be used.
*/
func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool, itemType ...string) (item *Item, err error) {


var prompt *Prompt var prompt *Prompt
var path dbus.ObjectPath var path dbus.ObjectPath
var promptPath dbus.ObjectPath var promptPath dbus.ObjectPath
var variant *dbus.Variant var variant *dbus.Variant
var props map[string]dbus.Variant = make(map[string]dbus.Variant) var props map[string]dbus.Variant = make(map[string]dbus.Variant)
var typeString string

if itemType != nil && len(itemType) > 0 {
typeString = itemType[0]
} else {
typeString = DbusDefaultItemType
}


props[DbusItemLabel] = dbus.MakeVariant(label) props[DbusItemLabel] = dbus.MakeVariant(label)
props[DbusItemType] = dbus.MakeVariant(typeString)
props[DbusItemAttributes] = dbus.MakeVariant(attrs) props[DbusItemAttributes] = dbus.MakeVariant(attrs)


if err = c.Dbus.Call( if err = c.Dbus.Call(
@ -177,8 +191,11 @@ func (c *Collection) Relabel(newLabel string) (err error) {


/* /*
SearchItems searches a Collection for a matching profile string. SearchItems searches a Collection for a matching profile string.
It's mostly a carry-over from go-libsecret, and is here for convenience. It's mostly a carry-over from go-libsecret, and is here for convenience. IT MAY BE REMOVED IN THE FUTURE.

I promise it's not useful for any other implementation/storage of SecretService whatsoever. I promise it's not useful for any other implementation/storage of SecretService whatsoever.

Deprecated: Use Service.SearchItems instead.
*/ */
func (c *Collection) SearchItems(profile string) (items []*Item, err error) { func (c *Collection) SearchItems(profile string) (items []*Item, err error) {



View File

@ -66,12 +66,13 @@ func TestCollection_Items(t *testing.T) {
} }


if collection, err = svc.GetCollection(defaultCollection); err != nil { if collection, err = svc.GetCollection(defaultCollection); err != nil {
if err = svc.Close(); err != nil { t.Errorf("failed when fetching collection '%v': %v",
t.Errorf("could not close Service.Session: %v", err.Error())
}
t.Fatalf("failed when fetching collection '%v': %v",
defaultCollection, err.Error(), defaultCollection, err.Error(),
) )
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} }


if items, err = collection.Items(); err != nil { if items, err = collection.Items(); err != nil {
@ -83,6 +84,22 @@ func TestCollection_Items(t *testing.T) {
t.Logf("found %v items in collection '%v' at '%v'", len(items), defaultCollection, string(collection.Dbus.Path())) t.Logf("found %v items in collection '%v' at '%v'", len(items), defaultCollection, string(collection.Dbus.Path()))
} }


/* This is almost always going to trigger the warning. See Item.idx for details why.
var label string
for idx, i := range items {
if label, err = i.Label(); err != nil {
t.Errorf("failed to get label of item '%v' in collection '%v': %v", string(i.Dbus.Path()), collectionName.String(), err.Error())
continue
}
if i.idx != idx {
t.Logf(
"WARN: item '%v' ('%v') in collection '%v' internal IDX ('%v') does NOT match native slice IDX ('%v')",
string(i.Dbus.Path()), label, collectionName.String(), i.idx, idx,
)
}
}
*/

secret = NewSecret(svc.Session, []byte{}, []byte(testSecretContent), "text/plain") secret = NewSecret(svc.Session, []byte{}, []byte(testSecretContent), "text/plain")


if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, false); err != nil { if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, false); err != nil {
@ -136,10 +153,10 @@ func TestCollection_Label(t *testing.T) {
"failed when fetching collection '%v': %v", "failed when fetching collection '%v': %v",
defaultCollectionLabel, err.Error(), defaultCollectionLabel, err.Error(),
) )
err = nil
if err = svc.Close(); err != nil { 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())
} }
return
} }


if collLabel, err = collection.Label(); err != nil { if collLabel, err = collection.Label(); err != nil {
@ -147,6 +164,7 @@ func TestCollection_Label(t *testing.T) {
if err = svc.Close(); err != nil { if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error()) t.Fatalf("could not close Service.Session: %v", err.Error())
} }
return
} }


if defaultCollectionLabel != collLabel { if defaultCollectionLabel != collLabel {
@ -182,10 +200,10 @@ func TestCollection_Locked(t *testing.T) {
"failed when fetching collection '%v': %v", "failed when fetching collection '%v': %v",
defaultCollectionLabel, err.Error(), defaultCollectionLabel, err.Error(),
) )
err = nil
if err = svc.Close(); err != nil { 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())
} }
return
} }


if isLocked, err = collection.Locked(); err != nil { if isLocked, err = collection.Locked(); err != nil {
@ -198,3 +216,71 @@ func TestCollection_Locked(t *testing.T) {
t.Errorf("could not close Service.Session: %v", err.Error()) t.Errorf("could not close Service.Session: %v", err.Error())
} }
} }

/*
TestCollection_Relabel tests the following internal functions/methods via nested calls:

(all calls in TestNewCollection)
Service.CreateCollection
Collection.Relabel

*/
func TestCollection_Relabel(t *testing.T) {

var svc *Service
var collection *Collection
var collLabel string
var newCollLabel string = collectionAlias.String()
var err error

if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %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.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
}

if collLabel, err = collection.Label(); err != nil {
t.Errorf("could not fetch label for collection '%v': %v", string(collection.Dbus.Path()), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}

if err = collection.Relabel(newCollLabel); err != nil {
t.Errorf("failed to relabel collection '%v' to '%v': %v", collLabel, newCollLabel, err.Error())
} else {
t.Logf("relabeled collection '%v' to '%v'", collLabel, newCollLabel)
}

if collLabel, err = collection.Label(); err != nil {
t.Errorf("could not fetch label for collection '%v': %v", string(collection.Dbus.Path()), err.Error())
if err = collection.Delete(); err != nil {
t.Errorf("failed to delete collection '%v': %v", string(collection.Dbus.Path()), err.Error())
}
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else {
if collLabel != newCollLabel {
t.Errorf("collection did not relabel; new label '%v', actual label '%v'", newCollLabel, collLabel)
}
}

if err = collection.Delete(); err != nil {
t.Errorf("failed to delete collection '%v': %v", string(collection.Dbus.Path()), err.Error())
}

if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}

View File

@ -18,6 +18,13 @@ const (
DbusServiceBase string = "org.freedesktop.Secret" DbusServiceBase string = "org.freedesktop.Secret"
// DbusPrompterInterface is an interface for issuing a Prompt. Yes, it should be doubled up like that. // DbusPrompterInterface is an interface for issuing a Prompt. Yes, it should be doubled up like that.
DbusPrompterInterface string = DbusServiceBase + ".Prompt.Prompt" DbusPrompterInterface string = DbusServiceBase + ".Prompt.Prompt"
/*
DbusDefaultItemType is the default type to use for Item.Type.
I've only ever seen "org.gnome.keyring.NetworkPassword" in the wild
aside from the below. It may be legacy (gnome-keyring is obsoleted by SecretService).
If in doubt, the below is considered the "proper" interface.
*/
DbusDefaultItemType string = DbusServiceBase + ".Generic"
) )


// Service interface. // Service interface.

6
doc.go
View File

@ -76,9 +76,9 @@ So the object hierarchy in THEORY looks kind of like this:
└─ Secret "B_2_b" └─ Secret "B_2_b"


And so on. And so on.
In PRACTICE, however, most users will only have two Session items In PRACTICE, however, most users will only have two Collection items
(a default "system" one and a temporary one that may or may not exist, running in memory for the current login session) (a default "system" one named "login", which usually is unlocked upon login,
and a single Collection, named "login" (and aliased to "default", usually). and a temporary one that may or may not exist, running in memory for the current login session named `session`).


Usage Usage



View File

@ -173,15 +173,9 @@ func (i *Item) Relabel(newLabel string) (err error) {
// ReplaceAttributes replaces the Item's attributes in Dbus. // ReplaceAttributes replaces the Item's attributes in Dbus.
func (i *Item) ReplaceAttributes(newAttrs map[string]string) (err error) { func (i *Item) ReplaceAttributes(newAttrs map[string]string) (err error) {


var label string var props dbus.Variant
var props map[string]dbus.Variant = make(map[string]dbus.Variant, 0)


if label, err = i.Label(); err != nil { props = dbus.MakeVariant(newAttrs)
return
}

props[DbusItemLabel] = dbus.MakeVariant(label)
props[DbusItemAttributes] = dbus.MakeVariant(newAttrs)


if err = i.Dbus.SetProperty(DbusItemAttributes, props); err != nil { if err = i.Dbus.SetProperty(DbusItemAttributes, props); err != nil {
return return
@ -217,8 +211,7 @@ func (i *Item) Type() (itemType string, err error) {
return return
} }


i.ItemType = variant.Value().(string) itemType = variant.Value().(string)
itemType = i.ItemType


return return
} }

150
item_funcs_test.go Normal file
View File

@ -0,0 +1,150 @@
package gosecret

import (
`reflect`
`testing`
)

// Some functions are covered in the Service tests and Collection tests.

/*
TestItem tests all remaining Item funcs (see Service and Collection funcs for the other tests.

*/
func TestItem(t *testing.T) {

var svc *Service
var collection *Collection
var item *Item
var secret *Secret
var newItemLabel string
var testLabel string
var attrs map[string]string
var modAttrs map[string]string
var newAttrs map[string]string
var newAttrsGnome map[string]string
var typeString string
var err error

// Setup.
if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %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.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
}

// Create an Item/Secret.
secret = NewSecret(svc.Session, []byte{}, []byte(testSecretContent), "text/plain")

if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, true); err != nil {
t.Errorf("could not create item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}

// Fetch attributes
if attrs, err = item.Attributes(); err != nil {
t.Errorf("failed to fetch attributes for item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
} else {
t.Logf(
"Fetch result; original attributes: %#v, fetched attributes: %#v for item '%v' in '%v'",
itemAttrs, attrs, testItemLabel, collectionName.String(),
)
}

newAttrs = map[string]string{
"foo": "bar",
"bar": "baz",
"baz": "quux",
}

// Replace attributes.
if err = item.ReplaceAttributes(newAttrs); err != nil {
t.Errorf("could not replace attributes for item '%v' in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
} else {
// Modify attributes.
// "flat" modification.
modAttrs = map[string]string{
"foo": "quux",
}
if err = item.ModifyAttributes(modAttrs); err != nil {
t.Errorf(
"could not modify attributes for item '%v' in collection '%v' (%#v => %#v): %v",
testItemLabel, collectionName.String(), newAttrs, modAttrs, err.Error(),
)
}
// "delete" modification.
newAttrs = map[string]string{
"foo": "quux",
"bar": "baz",
}
newAttrsGnome = make(map[string]string, 0)
for k, v := range newAttrs {
newAttrsGnome[k] = v
}
// Added via SecretService automatically? Seahorse? It appears sometimes and others it does not. Cause is unknown.
newAttrsGnome["xdg:schema"] = DbusDefaultItemType
modAttrs = map[string]string{
"baz": ExplicitAttrEmptyValue,
}

if err = item.ModifyAttributes(modAttrs); err != nil {
t.Errorf(
"could not modify (with deletion) attributes for item '%v' in collection '%v' (%#v => %#v): %v",
testItemLabel, collectionName.String(), newAttrs, modAttrs, err.Error(),
)
} else {
if attrs, err = item.Attributes(); err != nil {
t.Errorf("failed to fetch attributes for item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
}
if !reflect.DeepEqual(attrs, newAttrs) && !reflect.DeepEqual(attrs, newAttrsGnome) {
t.Errorf("newly-modified attributes (%#v) do not match expected attributes (%#v)", attrs, newAttrs)
} else {
t.Logf("modified attributes (%#v) match expected attributes (%#v)", attrs, newAttrs)
}
}
}

// Item.Relabel
newItemLabel = testItemLabel + "_RELABELED"
if err = item.Relabel(newItemLabel); err != nil {
t.Errorf("failed to relabel item '%v' to '%v': %v", testItemLabel, newItemLabel, err.Error())
}
if testLabel, err = item.Label(); err != nil {
t.Errorf("failed to fetch label for '%v': %v", string(item.Dbus.Path()), err.Error())
}
if newItemLabel != testLabel {
t.Errorf("new item label post-relabeling ('%v') does not match explicitly set label ('%v')", testLabel, newItemLabel)
}

// And Item.Type.
if typeString, err = item.Type(); err != nil {
t.Errorf("failed to get Item.Type for '%v': %v", string(item.Dbus.Path()), err.Error())
} else {
if typeString != DbusDefaultItemType {
t.Errorf("Item.Type mismatch for '%v': '%v' (should be '%v')", string(item.Dbus.Path()), typeString, DbusDefaultItemType)
}
t.Logf("item type for '%v': %v", string(item.Dbus.Path()), typeString)
}

// Teardown.
if err = item.Delete(); err != nil {
t.Errorf("failed to delete item '%v': %v", string(item.Dbus.Path()), err.Error())
}
if err = collection.Delete(); err != nil {
t.Errorf("failed to delete collection '%v': %v", collectionName.String(), err.Error())
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}

View File

@ -63,30 +63,31 @@ func TestService_Collections(t *testing.T) {
t.Errorf("could not get Service.Collections: %v", err.Error()) t.Errorf("could not get Service.Collections: %v", err.Error())
} else { } else {
t.Logf("found %v collections via Service.Collections", len(colls)) t.Logf("found %v collections via Service.Collections", len(colls))
}
for idx, c := range colls { for idx, c := range colls {
if collLabel, err = c.Label(); err != nil { if collLabel, err = c.Label(); err != nil {
t.Errorf( t.Errorf(
"failed to get label for collection '%v': %v", "failed to get label for collection '%v': %v",
string(c.Dbus.Path()), err.Error(), 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.PathName(), collLabel, created, modified,
) )
} }
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.PathName(), collLabel, created, modified,
)
} }


if err = svc.Close(); err != nil { if err = svc.Close(); err != nil {
@ -158,7 +159,7 @@ func TestService_CreateAliasedCollection(t *testing.T) {
Service.GetCollection Service.GetCollection
NewCollection NewCollection
Collection.Modified Collection.Modified
Service.ReadAlias Service.ReadAlias


The default collection (login) is fetched instead of creating one as this collection should exist, The default collection (login) is fetched instead of creating one as this collection should exist,
and thus this function tests fetching existing collections instead of newly-created ones. and thus this function tests fetching existing collections instead of newly-created ones.
@ -228,6 +229,7 @@ func TestService_Secrets(t *testing.T) {
if err = svc.Close(); err != nil { if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error()) t.Fatalf("could not close Service.Session: %v", err.Error())
} }
return
} else { } else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path())) t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
} }
@ -256,6 +258,7 @@ func TestService_Secrets(t *testing.T) {
if err = svc.Close(); err != nil { if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error()) t.Fatalf("could not close Service.Session: %v", err.Error())
} }
return
} }


if itemName, err = testItem.Label(); err != nil { if itemName, err = testItem.Label(); err != nil {

View File

@ -107,18 +107,14 @@ type Item struct {
*DbusObject *DbusObject
// Secret is the corresponding Secret object. // Secret is the corresponding Secret object.
Secret *Secret `json:"secret"` Secret *Secret `json:"secret"`
/*
ItemType is the type of this Item as a Dbus interface name.
e.g. org.gnome.keyring.NetworkPassword, org.freedesktop.Secret.Generic, org.remmina.Password, etc.
*/
ItemType string `json:"dbus_type"`
// lastModified is unexported because it's important that API users don't change it; it's used by Collection.Modified. // lastModified is unexported because it's important that API users don't change it; it's used by Collection.Modified.
lastModified time.Time lastModified time.Time
// lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not. // lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not.
lastModifiedSet bool lastModifiedSet bool
/* /*
idx is the index identifier of the Item. idx is the index identifier of the Item.
It SHOULD correlate to indices in Collection.Items, but don't rely on this. It is almost guaranteed to not match the index in Collection.Items (unless you have like, only one item)
as those indices are static and do not determine the order that Dbus returns the list of item paths.
*/ */
idx int idx int
// collection tracks the Collection this Item is in. // collection tracks the Collection this Item is in.