Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce ReadOnlyFileStore #850

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

programmer04
Copy link

@programmer04 programmer04 commented Nov 27, 2024

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

Hence, to overcome the aforementioned problems, this PR introduces a new type of a store - ReadOnlyFileStore that can be populated from io.Reader and supports only Get(...) method according to the proposal from @Wwwsylvia described in the #850 (comment)

@Wwwsylvia
Copy link
Member

Hi @programmer04, thank you for the PR!

From the original issue Kong/gateway-operator#521, it looks like your scenario involves reading plaintext credentials from regcred without needing to update or delete them. Is that correct?

If so, rather than modifying the original credentials file store implementation, we could introduce something like ReadOnlyFileStore, which allows creating a store from a reader and does not support write operations on the credentials.

To get unblocked, you can consider implementing the ReadOnlyFileStore type in your code base. Since the plan is to support read operations only on the auth entry, the implementation would be much more straightforward than the current implementation in oras-go (no need to worry about concurrent writes, accidental overwrites, etc.).

If we feel like this functionality is necessary to be in oras-go, we can open an issue to track the feature request. What do you think?

@programmer04
Copy link
Author

Hey @Wwwsylvia! Thanks for the response

Yes, you're right. The problem I am trying to resolve is populating an oras credential store robustly directly from in-memory (io.Reader) a plain text representation of standard JSON Docker credentials. It can be read-only, it's sufficient, but it should be as good as FileStore in handling all of the cases, see the below.

Here in FileStore operation Get is implemented like that

// Get retrieves credentials from the store for the given server address.
func (fs *FileStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) {
return fs.config.GetCredential(serverAddress)
}

where
// GetAuthConfig returns an auth.Credential for serverAddress.
func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error) {
cfg.rwLock.RLock()
defer cfg.rwLock.RUnlock()
authCfgBytes, ok := cfg.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 {
if toHostname(addr) == serverAddress {
matched = true
authCfgBytes = auth
break
}
}
if !matched {
return auth.EmptyCredential, nil
}
}
var authCfg AuthConfig
if err := json.Unmarshal(authCfgBytes, &authCfg); err != nil {
return auth.EmptyCredential, fmt.Errorf("failed to unmarshal auth field: %w: %v", ErrInvalidConfigFormat, err)
}
return authCfg.Credential()
}

contains logic to make it work for all cases.

There is memoryStore, but I don't see a way how easily implement generating such a store from standard JSON Docker credentials than handles all of edge cases, etc. like FileStore with its underlying Config struct that is coupled with actual file by a path field.

The problem is that there is a path field in Config (this structure maps JSON Docker creds). It's hard to break that coupling, this is why I proposed a workaround in PR to avoid breaking anything. Maybe you have an idea of how to tackle it differently?

@Wwwsylvia
Copy link
Member

Wwwsylvia commented Dec 18, 2024

The problem is that there is a path field in Config (this structure maps JSON Docker creds). It's hard to break that coupling, this is why I proposed a workaround in PR to avoid breaking anything. Maybe you have an idea of how to tackle it differently?

The file store and the internal Config struct was designed for reading/writing Docker config files. If read-only is sufficient, we can implement a new store like ReadOnlyFileStore that loads the config from a reader. To support such store, we also need to implement a new Config like ReadOnlyConfig. This way we won't add complexity to the existing implementation and can gain better performance in the read-only scenario.

It would look somewhat like:

type ReadOnlyConfig struct {
	Auths map[string]AuthConfig
}

func LoadFromReader(reader io.Reader) (*ReadOnlyConfig, error) {
	// TODO: read from the reader and decode the content into Auths
	return &ReadOnlyConfig{}, nil
}

func (c *ReadOnlyConfig) GetCredential(serverAddress string) (auth.Credential, error) {
	// TODO: get the credential from Auths with best effort
	return auth.Credential{}, nil
}
type ReadOnlyFileStore struct {
	*config.ReadOnlyConfig
}

func NewReadOnlyFileStoreFromReader(reader io.Reader) (*ReadOnlyFileStore, error) {
	cfg, err := config.LoadFromReader(reader)
	if err != nil {
		return nil, err
	}
	return &ReadOnlyFileStore{ReadOnlyConfig: cfg}, nil
}

func (fs *ReadOnlyFileStore) Get(serverAddress string) (auth.Credential, error) {
	return fs.ReadOnlyConfig.GetCredential(serverAddress)
}

func (fs *ReadOnlyFileStore) Put(serverAddress string, cred auth.Credential) error {
	return errors.New("cannot put credentials in read-only file store")
}

func (fs *ReadOnlyFileStore) Delete(serverAddress string) error {
	return errors.New("cannot delete credentials in read-only file store")
}

@programmer04 programmer04 changed the title feat: allow loading config from io.Reader feat: introduce ReadOnlyFileStore Dec 18, 2024
@programmer04 programmer04 force-pushed the cfg-from-reader branch 4 times, most recently from 5ceabd4 to 30e4961 Compare December 18, 2024 15:01
@programmer04
Copy link
Author

programmer04 commented Dec 18, 2024

Thanks for the detailed guidance @Wwwsylvia I've updated PR (code, title, description) according to the solution proposed by you in #850 (comment) I even tested it with my code Kong/gateway-operator#940. Please take a look

registry/remote/credentials/internal/config/config.go Outdated Show resolved Hide resolved
registry/remote/credentials/file_store.go Outdated Show resolved Hide resolved
registry/remote/credentials/file_store.go Outdated Show resolved Hide resolved
@programmer04 programmer04 force-pushed the cfg-from-reader branch 2 times, most recently from de2c43a to 0ad13e6 Compare December 20, 2024 12:31
@programmer04 programmer04 force-pushed the cfg-from-reader branch 2 times, most recently from b70d35b to 83d5c64 Compare January 9, 2025 10:51
Copy link

codecov bot commented Jan 9, 2025

Codecov Report

Attention: Patch coverage is 67.56757% with 12 lines in your changes missing coverage. Please review.

Project coverage is 75.57%. Comparing base (eeb21fc) to head (83d5c64).

Files with missing lines Patch % Lines
...ote/credentials/internal/config/readonly_config.go 0.00% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #850      +/-   ##
==========================================
- Coverage   75.67%   75.57%   -0.11%     
==========================================
  Files          63       66       +3     
  Lines        6011     6035      +24     
==========================================
+ Hits         4549     4561      +12     
- Misses       1079     1091      +12     
  Partials      383      383              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

registry/remote/credentials/internal/config/helpers.go Outdated Show resolved Hide resolved
registry/remote/credentials/internal/config/helpers.go Outdated Show resolved Hide resolved
registry/remote/credentials/file_store_test.go Outdated Show resolved Hide resolved
Comment on lines 54 to 62
// 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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For type safety, maybe we should introduce a new interface ReadOnlyStore that only contains the Get method.

type ReadOnlyStore interface {
	// Get retrieves credentials from the store for the given server address.
	Get(ctx context.Context, serverAddress string) (auth.Credential, error)
}

We can also change the signature of Credential() from

func Credential(store Store) auth.CredentialFunc {

to

func Credential(store ReadOnlyStore) auth.CredentialFunc {

@programmer04 Do you use the Credential() function in your code?
@shizhMSFT What do you think of adding a new interface?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great idea! I implemented this and tested it with my code - Kong/gateway-operator#940 (there is a usage of Credential()) and it works nicely.

Furthermore, I implemented all other suggestions. PTAL

@programmer04 programmer04 force-pushed the cfg-from-reader branch 4 times, most recently from e7582ea to f8e6d98 Compare January 9, 2025 17:10
Signed-off-by: Jakub Warczarek <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants