Skip to content

Commit

Permalink
feat: introduce ReadOnlyFileStore
Browse files Browse the repository at this point in the history
Signed-off-by: Jakub Warczarek <[email protected]>
  • Loading branch information
programmer04 committed Dec 18, 2024
1 parent e4dc64e commit 30e4961
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 6 deletions.
37 changes: 37 additions & 0 deletions registry/remote/credentials/file_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"context"
"errors"
"fmt"
"io"
"strings"

"oras.land/oras-go/v2/registry/remote/auth"
Expand All @@ -44,6 +45,9 @@ var (
// ErrBadCredentialFormat is returned by Put() when the credential format
// is bad.
ErrBadCredentialFormat = errors.New("bad credential format")
// ErrReadOnlyStore is returned for operations
// Put(...) and Delete(...) for read-only store.
ErrReadOnlyStore = errors.New("cannot modify content of the read-only store")
)

// NewFileStore creates a new file credentials store.
Expand Down Expand Up @@ -95,3 +99,36 @@ func validateCredentialFormat(cred auth.Credential) error {
}
return nil
}

// ReadOnlyFileStore implements a credentials store using the docker configuration file
// as an input. It supports only Get operation that works in the same way as for standard
// FileStore.
type ReadOnlyFileStore struct {
cfg *config.ReadOnlyConfig
}

// NewFileStore creates a new file credentials store based on the given config,
// it returns an error if the config is not in the expected format.
func NewReadOnlyFileStore(reader io.Reader) (*ReadOnlyFileStore, error) {
cfg, err := config.NewReadOnlyConfig(reader)
if err != nil {
return nil, err
}
return &ReadOnlyFileStore{cfg: cfg}, nil
}

// Get retrieves credentials from the store for the given server address. In case of non-existent
// server address, it returns auth.EmptyCredential.
func (fs *ReadOnlyFileStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) {
return fs.cfg.GetCredential(serverAddress)
}

// Get always returns ErrReadOnlyStore. It's present to satisfy the Store interface.
func (fs *ReadOnlyFileStore) Put(_ context.Context, _ string, _ auth.Credential) error {
return ErrReadOnlyStore
}

// Delete always returns ErrReadOnlyStore. It's present to satisfy the Store interface.
func (fs *ReadOnlyFileStore) Delete(_ context.Context, _ string) error {
return ErrReadOnlyStore
}
88 changes: 84 additions & 4 deletions registry/remote/credentials/file_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.
package credentials

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand All @@ -25,6 +26,7 @@ import (
"testing"

"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config/configtest"
)

Expand Down Expand Up @@ -81,23 +83,46 @@ func TestNewFileStore_badFormat(t *testing.T) {
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run(tt.name+" FileStore", func(t *testing.T) {
_, err := NewFileStore(tt.configPath)
if (err != nil) != tt.wantErr {
t.Errorf("NewFileStore() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
t.Run(tt.name+" ReadOnlyFileStore", func(t *testing.T) {
f, err := os.Open(tt.configPath)
if err != nil {
t.Fatalf("failed to open file: %v", err)
}
defer f.Close()

_, err = NewReadOnlyFileStore(f)
if (err != nil) != tt.wantErr {
t.Errorf("NewReadOnlyFileStore() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

func TestFileStore_Get_validConfig(t *testing.T) {
func TestFileStoreAndReadOnlyFileStore_Get_validConfig(t *testing.T) {
ctx := context.Background()
fs, err := NewFileStore("testdata/valid_auths_config.json")
const validAuthsConfigPath = "testdata/valid_auths_config.json"
fs, err := NewFileStore(validAuthsConfigPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}

f, err := os.ReadFile(validAuthsConfigPath)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
rofs, err := NewReadOnlyFileStore(bytes.NewReader(f))
if err != nil {
t.Fatalf("NewReadOnlyFileStore() error = %v", err)
}

tests := []struct {
name string
serverAddress string
Expand Down Expand Up @@ -169,7 +194,7 @@ func TestFileStore_Get_validConfig(t *testing.T) {
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run(tt.name+" FileStore.Get()", func(t *testing.T) {
got, err := fs.Get(ctx, tt.serverAddress)
if (err != nil) != tt.wantErr {
t.Errorf("FileStore.Get() error = %v, wantErr %v", err, tt.wantErr)
Expand All @@ -179,6 +204,27 @@ func TestFileStore_Get_validConfig(t *testing.T) {
t.Errorf("FileStore.Get() = %v, want %v", got, tt.want)
}
})
t.Run(tt.name+" ReadOnlyFileStore.Get()", func(t *testing.T) {
got, err := rofs.Get(ctx, tt.serverAddress)
if (err != nil) != tt.wantErr {
t.Errorf("ReadOnlyFileStore.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReadOnlyFileStore.Get() = %v, want %v", got, tt.want)
}
})
}
}

func TestReadOnlyFileStore_Create_fromInvalidConfig(t *testing.T) {
f, err := os.ReadFile("testdata/invalid_auths_entry_config.json")
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
_, err = NewReadOnlyFileStore(bytes.NewReader(f))
if !errors.Is(err, config.ErrInvalidConfigFormat) {
t.Fatalf("Error: %s is expected", config.ErrInvalidConfigFormat)
}
}

Expand Down Expand Up @@ -296,6 +342,23 @@ func TestFileStore_Get_notExistConfig(t *testing.T) {
}
}

func TestReadOnlyFileStore_Put_expectError(t *testing.T) {
const validAuthsConfigPath = "testdata/valid_auths_config.json"
f, err := os.ReadFile(validAuthsConfigPath)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}

rofs, err := NewReadOnlyFileStore(bytes.NewReader(f))
if err != nil {
t.Fatalf("NewReadOnlyFileStore() error = %v", err)
}
err = rofs.Put(context.Background(), "registry.example.com", auth.Credential{})
if !errors.Is(err, ErrReadOnlyStore) {
t.Fatalf("Error: %s is expected", ErrReadOnlyStore)
}
}

func TestFileStore_Put_notExistConfig(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
Expand Down Expand Up @@ -592,6 +655,23 @@ func TestFileStore_Put_passwordContainsColon(t *testing.T) {
}
}

func TestReadOnlyFileStore_Delete_expectError(t *testing.T) {
const validAuthsConfigPath = "testdata/valid_auths_config.json"
f, err := os.ReadFile(validAuthsConfigPath)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}

rofs, err := NewReadOnlyFileStore(bytes.NewReader(f))
if err != nil {
t.Fatalf("NewReadOnlyFileStore() error = %v", err)
}
err = rofs.Delete(context.Background(), "registry.example.com")
if !errors.Is(err, ErrReadOnlyStore) {
t.Fatalf("Error: %s is expected", ErrReadOnlyStore)
}
}

func TestFileStore_Delete(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
Expand Down
9 changes: 7 additions & 2 deletions registry/remote/credentials/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,18 @@ func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error)
cfg.rwLock.RLock()
defer cfg.rwLock.RUnlock()

authCfgBytes, ok := cfg.authsCache[serverAddress]
return getCredentialFromCache(cfg.authsCache, serverAddress)
}

// getCredentialsFromCache is a helper function to get the credential for serverAddress from authsCache in a raw format.
func getCredentialFromCache(authsCache map[string]json.RawMessage, serverAddress string) (auth.Credential, error) {
authCfgBytes, ok := authsCache[serverAddress]
if !ok {
// NOTE: the auth key for the server address may have been stored with
// a http/https prefix in legacy config files, e.g. "registry.example.com"
// can be stored as "https://registry.example.com/".
var matched bool
for addr, auth := range cfg.authsCache {
for addr, auth := range authsCache {
if toHostname(addr) == serverAddress {
matched = true
authCfgBytes = auth
Expand Down
67 changes: 67 additions & 0 deletions registry/remote/credentials/internal/config/readonly_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package config

import (
"encoding/json"
"fmt"
"io"

"oras.land/oras-go/v2/registry/remote/auth"
)

// ReadOnlyConfig represents authentication credentials parsed from a standard config file,
// which are read to use. It is read-only - only GetCredential is supported.
type ReadOnlyConfig struct {
auths map[string]auth.Credential
}

// NewReadOnlyConfig creates a new ReadOnlyConfig from the given reader that contains a standard
// config file content. It returns an error if the content is not in the expected format.
func NewReadOnlyConfig(reader io.Reader) (*ReadOnlyConfig, error) {
var content map[string]json.RawMessage
if err := json.NewDecoder(reader).Decode(&content); err != nil {
return nil, err
}
var authsCache map[string]json.RawMessage
if authsBytes, ok := content[configFieldAuths]; ok {
if err := json.Unmarshal(authsBytes, &authsCache); err != nil {
return nil, fmt.Errorf("failed to unmarshal auths field: %w: %v", ErrInvalidConfigFormat, err)
}
}

cfg := ReadOnlyConfig{
auths: make(map[string]auth.Credential, len(authsCache)),
}
for serverAddress := range authsCache {
creds, err := getCredentialFromCache(authsCache, serverAddress)
if err != nil {
return nil, err
}
cfg.auths[serverAddress] = creds
}

return &cfg, nil
}

// GetCredential returns the credential for the given server address. For non-existent server address,
// it returns auth.EmptyCredential.
func (cfg *ReadOnlyConfig) GetCredential(serverAddress string) (auth.Credential, error) {
if v, ok := cfg.auths[serverAddress]; ok {
return v, nil
}
return auth.EmptyCredential, nil
}

0 comments on commit 30e4961

Please sign in to comment.