Skip to content

Commit a15da41

Browse files
authored
feat: introduce credentials.NewMemoryStoreFromDockerConfig (#850)
**What this PR does / why we need it:** Currently, struct `Config` that represents a docker configuration file can be populated only with the function `Load(configPath string) (*Config, error)` that expects to have a file available on the disk and read it. Sometimes, this is a redundant step and a security risk when file content is available in memory. Here is a real-world example - Kong/kong-operator#521 Hence, to overcome the aforementioned problems, this PR introduces a new constructor - `NewMemoryStoreFromDockerConfig` that can populate `memoryStore` from standard config in the form of `[]byte`. The proposal from @Wwwsylvia and @shizhMSFT described in the #850 (comment) Signed-off-by: Jakub Warczarek <jakub.warczarek@gmail.com>
1 parent 1a9e250 commit a15da41

4 files changed

Lines changed: 199 additions & 4 deletions

File tree

registry/remote/credentials/internal/config/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error)
167167
// can be stored as "https://registry.example.com/".
168168
var matched bool
169169
for addr, auth := range cfg.authsCache {
170-
if toHostname(addr) == serverAddress {
170+
if ToHostname(addr) == serverAddress {
171171
matched = true
172172
authCfgBytes = auth
173173
break
@@ -319,12 +319,12 @@ func decodeAuth(authStr string) (username string, password string, err error) {
319319
return username, password, nil
320320
}
321321

322-
// toHostname normalizes a server address to just its hostname, removing
322+
// ToHostname normalizes a server address to just its hostname, removing
323323
// the scheme and the path parts.
324324
// It is used to match keys in the auths map, which may be either stored as
325325
// hostname or as hostname including scheme (in legacy docker config files).
326326
// Reference: https://github.com/docker/cli/blob/v24.0.6/cli/config/credentials/file_store.go#L71
327-
func toHostname(addr string) string {
327+
func ToHostname(addr string) string {
328328
addr = strings.TrimPrefix(addr, "http://")
329329
addr = strings.TrimPrefix(addr, "https://")
330330
addr, _, _ = strings.Cut(addr, "/")

registry/remote/credentials/internal/config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1444,7 +1444,7 @@ func Test_toHostname(t *testing.T) {
14441444
}
14451445
for _, tt := range tests {
14461446
t.Run(tt.name, func(t *testing.T) {
1447-
if got := toHostname(tt.addr); got != tt.want {
1447+
if got := ToHostname(tt.addr); got != tt.want {
14481448
t.Errorf("toHostname() = %v, want %v", got, tt.want)
14491449
}
14501450
})

registry/remote/credentials/memory_store.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ package credentials
1717

1818
import (
1919
"context"
20+
"encoding/json"
21+
"fmt"
2022
"sync"
2123

2224
"oras.land/oras-go/v2/registry/remote/auth"
25+
"oras.land/oras-go/v2/registry/remote/credentials/internal/config"
2326
)
2427

2528
// memoryStore is a store that keeps credentials in memory.
@@ -32,6 +35,30 @@ func NewMemoryStore() Store {
3235
return &memoryStore{}
3336
}
3437

38+
// NewMemoryStoreFromDockerConfig creates a new in-memory credentials store from the given configuration.
39+
//
40+
// Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
41+
func NewMemoryStoreFromDockerConfig(c []byte) (Store, error) {
42+
cfg := struct {
43+
Auths map[string]config.AuthConfig `json:"auths"`
44+
}{}
45+
if err := json.Unmarshal(c, &cfg); err != nil {
46+
return nil, fmt.Errorf("failed to unmarshal auth field: %w: %v", config.ErrInvalidConfigFormat, err)
47+
}
48+
49+
s := &memoryStore{}
50+
for addr, auth := range cfg.Auths {
51+
// Normalize the auth key to hostname.
52+
hostname := config.ToHostname(addr)
53+
cred, err := auth.Credential()
54+
if err != nil {
55+
return nil, err
56+
}
57+
_, _ = s.store.LoadOrStore(hostname, cred)
58+
}
59+
return s, nil
60+
}
61+
3562
// Get retrieves credentials from the store for the given server address.
3663
func (ms *memoryStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) {
3764
cred, found := ms.store.Load(serverAddress)
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
Copyright The ORAS Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package credentials
17+
18+
import (
19+
"context"
20+
"errors"
21+
"os"
22+
"reflect"
23+
"testing"
24+
25+
"oras.land/oras-go/v2/registry/remote/auth"
26+
"oras.land/oras-go/v2/registry/remote/credentials/internal/config"
27+
)
28+
29+
func TestMemoryStore_Create_fromInvalidConfig(t *testing.T) {
30+
f, err := os.ReadFile("testdata/invalid_auths_entry_config.json")
31+
if err != nil {
32+
t.Fatalf("failed to read file: %v", err)
33+
}
34+
_, err = NewMemoryStoreFromDockerConfig(f)
35+
if !errors.Is(err, config.ErrInvalidConfigFormat) {
36+
t.Fatalf("Error: %s is expected", config.ErrInvalidConfigFormat)
37+
}
38+
}
39+
40+
func TestMemoryStore_Get_validConfig(t *testing.T) {
41+
ctx := context.Background()
42+
f, err := os.ReadFile("testdata/valid_auths_config.json")
43+
if err != nil {
44+
t.Fatalf("failed to read file: %v", err)
45+
}
46+
cfg, err := NewMemoryStoreFromDockerConfig(f)
47+
if err != nil {
48+
t.Fatalf("NewMemoryStoreFromConfig() error = %v", err)
49+
}
50+
51+
tests := []struct {
52+
name string
53+
serverAddress string
54+
want auth.Credential
55+
wantErr bool
56+
}{
57+
{
58+
name: "Username and password",
59+
serverAddress: "registry1.example.com",
60+
want: auth.Credential{
61+
Username: "username",
62+
Password: "password",
63+
},
64+
},
65+
{
66+
name: "Identity token",
67+
serverAddress: "registry2.example.com",
68+
want: auth.Credential{
69+
RefreshToken: "identity_token",
70+
},
71+
},
72+
{
73+
name: "Registry token",
74+
serverAddress: "registry3.example.com",
75+
want: auth.Credential{
76+
AccessToken: "registry_token",
77+
},
78+
},
79+
{
80+
name: "Username and password, identity token and registry token",
81+
serverAddress: "registry4.example.com",
82+
want: auth.Credential{
83+
Username: "username",
84+
Password: "password",
85+
RefreshToken: "identity_token",
86+
AccessToken: "registry_token",
87+
},
88+
},
89+
{
90+
name: "Empty credential",
91+
serverAddress: "registry5.example.com",
92+
want: auth.EmptyCredential,
93+
},
94+
{
95+
name: "Username and password, no auth",
96+
serverAddress: "registry6.example.com",
97+
want: auth.Credential{
98+
Username: "username",
99+
Password: "password",
100+
},
101+
},
102+
{
103+
name: "Auth overriding Username and password",
104+
serverAddress: "registry7.example.com",
105+
want: auth.Credential{
106+
Username: "username",
107+
Password: "password",
108+
},
109+
},
110+
{
111+
name: "Not in auths",
112+
serverAddress: "foo.example.com",
113+
want: auth.EmptyCredential,
114+
},
115+
{
116+
name: "No record",
117+
serverAddress: "registry999.example.com",
118+
want: auth.EmptyCredential,
119+
},
120+
}
121+
for _, tt := range tests {
122+
t.Run(tt.name+" MemoryStore.Get()", func(t *testing.T) {
123+
got, err := cfg.Get(ctx, tt.serverAddress)
124+
if (err != nil) != tt.wantErr {
125+
t.Errorf("MemoryStore.Get() error = %v, wantErr %v", err, tt.wantErr)
126+
return
127+
}
128+
if !reflect.DeepEqual(got, tt.want) {
129+
t.Errorf("MemoryStore.Get() = %v, want %v", got, tt.want)
130+
}
131+
})
132+
}
133+
}
134+
135+
func TestMemoryStore_Get_emptyConfig(t *testing.T) {
136+
ctx := context.Background()
137+
emptyValidJson := []byte("{}")
138+
cfg, err := NewMemoryStoreFromDockerConfig(emptyValidJson)
139+
if err != nil {
140+
t.Fatal("NewMemoryStoreFromConfig() error =", err)
141+
}
142+
143+
tests := []struct {
144+
name string
145+
serverAddress string
146+
want auth.Credential
147+
wantErr error
148+
}{
149+
{
150+
name: "Not found",
151+
serverAddress: "registry.example.com",
152+
want: auth.EmptyCredential,
153+
wantErr: nil,
154+
},
155+
}
156+
for _, tt := range tests {
157+
t.Run(tt.name, func(t *testing.T) {
158+
got, err := cfg.Get(ctx, tt.serverAddress)
159+
if !errors.Is(err, tt.wantErr) {
160+
t.Errorf("MemoryStore.Get() error = %v, wantErr %v", err, tt.wantErr)
161+
return
162+
}
163+
if !reflect.DeepEqual(got, tt.want) {
164+
t.Errorf("MemoryStore.Get() = %v, want %v", got, tt.want)
165+
}
166+
})
167+
}
168+
}

0 commit comments

Comments
 (0)