Skip to content

Commit a42dca5

Browse files
authored
Merge pull request #108 from docker/store-filter
store: add Filter function
2 parents ddd4334 + 6e55c45 commit a42dca5

File tree

9 files changed

+577
-112
lines changed

9 files changed

+577
-112
lines changed

go.work.sum

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
22
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
3+
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
34
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
5+
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
46
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
57
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
68
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
79
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
810
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
11+
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
12+
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
913
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
14+
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
1015
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
1116
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
17+
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk=
1218
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
19+
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
1320
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
1421
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
1522
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
@@ -26,7 +33,23 @@ github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm
2633
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
2734
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
2835
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
36+
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
37+
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
38+
github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA=
39+
github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q=
40+
github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8=
41+
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
42+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
43+
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
44+
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
45+
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
46+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
47+
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
48+
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
49+
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
50+
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
2951
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
52+
github.com/opencontainers/runtime-tools v0.9.0 h1:FYgwVsKRI/H9hU32MJ/4MLOzXWodKK5zsQavY8NPMkU=
3053
github.com/opencontainers/runtime-tools v0.9.0/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
3154
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
3255
github.com/planetscale/vtprotobuf v0.4.0/go.mod h1:wm1N3qk9G/4+VM1WhpkLbvY/d8+0PbwYYpP5P5VhTks=
@@ -36,7 +59,12 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
3659
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
3760
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
3861
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
62+
github.com/planetscale/vtprotobuf v0.4.0 h1:NEI+g4woRaAZgeZ3sAvbtyvMBRjIv5kE7EWYQ8m4JwY=
63+
github.com/planetscale/vtprotobuf v0.4.0/go.mod h1:wm1N3qk9G/4+VM1WhpkLbvY/d8+0PbwYYpP5P5VhTks=
64+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
65+
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
3966
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
67+
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
4068
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
4169
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
4270
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
@@ -47,17 +75,28 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
4775
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
4876
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
4977
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
78+
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
79+
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
80+
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
81+
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
82+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
5083
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
5184
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
5285
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
5386
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
5487
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
88+
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
5589
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
5690
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
5791
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
92+
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
93+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
94+
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
5895
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
5996
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
6097
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8=
6198
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y=
99+
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ=
62100
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
101+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
63102
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

store/keychain/keychain.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,30 @@ const (
6767
secretIDKey = "id"
6868
)
6969

70-
// safelySetMetadata prefixes each key with `x_` so that no collissions can ever
71-
// occur with internal fields.
72-
func (k *keychainStore[T]) safelySetMetadata(id string, attributes map[string]string) {
73-
// prefix whatever is already in attributes
70+
// safelySetMetadata is a helper function to keychain providers
71+
// it adds internal metadata as well as prefixes externally defined attribute
72+
// keys with `x_` so that no collissions can ever occur.
73+
func safelySetMetadata(serviceGroup, serviceName string, attributes map[string]string) {
74+
// we need to collect all keys first otherwise we might double set the prefix
7475
keys := slices.Collect(maps.Keys(attributes))
76+
// prefix whatever is already in attributes
7577
for _, k := range keys {
7678
attributes["x_"+k] = attributes[k]
7779
delete(attributes, k)
7880
}
7981

80-
attributes[serviceGroupKey] = k.serviceGroup
81-
attributes[serviceNameKey] = k.serviceName
82-
if id != "" {
83-
attributes[secretIDKey] = id
84-
}
82+
attributes[serviceGroupKey] = serviceGroup
83+
attributes[serviceNameKey] = serviceName
8584
}
8685

8786
// safelyCleanMetadata removes internal metadata and removes the `x_` prefix
8887
// on all keys containing it.
89-
func (k *keychainStore[T]) safelyCleanMetadata(attributes map[string]string) {
88+
func safelyCleanMetadata(attributes map[string]string) {
9089
delete(attributes, serviceGroupKey)
9190
delete(attributes, serviceNameKey)
9291
delete(attributes, secretIDKey)
9392

93+
// we need to collect all keys first otherwise we might double set the prefix
9494
keys := slices.Collect(maps.Keys(attributes))
9595
for _, key := range keys {
9696
after, found := strings.CutPrefix(key, "x_")
@@ -105,3 +105,13 @@ func (k *keychainStore[T]) safelyCleanMetadata(attributes map[string]string) {
105105
delete(attributes, key)
106106
}
107107
}
108+
109+
// safelySetID stores the id inside the attributes
110+
func safelySetID(id store.ID, attributes map[string]string) {
111+
// first check if the "id" key already exists, it's possibly set by the
112+
// caller, so we should avoid overwriting it.
113+
if v, ok := attributes[secretIDKey]; ok {
114+
attributes["x_"+secretIDKey] = v
115+
}
116+
attributes[secretIDKey] = id.String()
117+
}

store/keychain/keychain_darwin.go

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func (k *keychainStore[T]) Get(_ context.Context, id store.ID) (store.Secret, er
9898
if err != nil {
9999
return nil, err
100100
}
101-
k.safelyCleanMetadata(attributes)
101+
safelyCleanMetadata(attributes)
102102

103103
secret := k.factory()
104104
if err := secret.SetMetadata(attributes); err != nil {
@@ -133,7 +133,7 @@ func (k *keychainStore[T]) GetAllMetadata(context.Context) (map[string]store.Sec
133133
if err != nil {
134134
return nil, err
135135
}
136-
k.safelyCleanMetadata(attributes)
136+
safelyCleanMetadata(attributes)
137137

138138
secret := k.factory()
139139
if err := secret.SetMetadata(attributes); err != nil {
@@ -158,7 +158,8 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
158158

159159
metadata := make(map[string]string)
160160
maps.Copy(metadata, secret.Metadata())
161-
k.safelySetMetadata(id.String(), metadata)
161+
safelySetMetadata(k.serviceGroup, k.serviceName, metadata)
162+
safelySetID(id, metadata)
162163

163164
metadataAny := make(map[string]any)
164165
for k, v := range metadata {
@@ -169,6 +170,77 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
169170
return mapKeychainError(kc.AddItem(item))
170171
}
171172

173+
func (k *keychainStore[T]) Filter(_ context.Context, pattern store.Pattern) (map[string]store.Secret, error) {
174+
// Note: Filter on macOS cannot filter by generic attributes and thus we
175+
// cannot split the ID and store it in the keychain as parts for later
176+
// pattern matching.
177+
// We only have access to:
178+
// - "Account" (secrets.ID)
179+
// - "ServiceName" this keychain instances' serviceName
180+
// - "ServiceGroup" this keychain instances' serviceGroup
181+
//
182+
// Filtering happens after we have retrieved the secrets from the store
183+
// based on the above attributes.
184+
// We then match the IDs against the pattern, 1 by 1.
185+
// This shouldn't be too expensive since we don't actually retrieve the
186+
// encrypted secret when fetching many secrets. Only after they match
187+
// the pattern, do we fetch their data and possibly prompt the user.
188+
189+
item := newKeychainItem("", k)
190+
191+
// We use the MatchLimitAll attribute to query for multiple items from the
192+
// store. It cannot be used with item.SetReturnData.
193+
// https://developer.apple.com/documentation/security/secitemcopymatching(_:_:)#Discussion
194+
item.SetMatchLimit(kc.MatchLimitAll)
195+
196+
results, err := kc.QueryItem(item)
197+
if err != nil {
198+
return nil, mapKeychainError(err)
199+
}
200+
201+
creds := make(map[string]store.Secret)
202+
for _, result := range results {
203+
// it is possible that someone else has stored secrets in the keychain
204+
// directly without conforming to the store.ID format.
205+
// We shouldn't error here when these values cannot be retrieved or
206+
// parsed. Instead we just ignore them and proceed.
207+
// I guess in future we could at least log them somewhere?
208+
// but for now, let's just continue with the other items in the store.
209+
id, err := store.ParseID(result.Account)
210+
if err != nil {
211+
continue
212+
}
213+
214+
// filter out any secrets based on the pattern which we couldn't do
215+
// with the keychain API
216+
if !pattern.Match(id) {
217+
continue
218+
}
219+
220+
attr, err := convertAttributes(result.Attributes)
221+
if err != nil {
222+
return nil, err
223+
}
224+
safelyCleanMetadata(attr)
225+
226+
i, err := getItemWithData(id.String(), k)
227+
if err != nil {
228+
return nil, err
229+
}
230+
231+
secret := k.factory()
232+
if err := secret.SetMetadata(attr); err != nil {
233+
return nil, err
234+
}
235+
if err := secret.Unmarshal(i.Data); err != nil {
236+
return nil, err
237+
}
238+
creds[id.String()] = secret
239+
}
240+
241+
return creds, nil
242+
}
243+
172244
func mapKeychainError(err error) error {
173245
if err == nil {
174246
return nil

0 commit comments

Comments
 (0)