-
Notifications
You must be signed in to change notification settings - Fork 99
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
base: main
Are you sure you want to change the base?
Conversation
2f04665
to
52b94a7
Compare
52b94a7
to
70cebfa
Compare
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 If so, rather than modifying the original credentials file store implementation, we could introduce something like To get unblocked, you can consider implementing the If we feel like this functionality is necessary to be in |
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 ( Here in oras-go/registry/remote/credentials/file_store.go Lines 65 to 68 in 52b94a7
where oras-go/registry/remote/credentials/internal/config/config.go Lines 171 to 198 in 52b94a7
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 The problem is that there is a |
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 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")
} |
70cebfa
to
57fdb2e
Compare
ReadOnlyFileStore
5ceabd4
to
30e4961
Compare
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 |
|
||
// 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should this be called LoadFromReader
to be consistent with Load
??
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the config is read-only and we don't need to write anything back, we can simply decode the content into auths
for read. How about:
func LoadFromReader(reader io.Reader) (*ReadOnlyConfig, error) {
cfg := &ReadOnlyConfig{}
if err := json.NewDecoder(reader).Decode(&cfg.auths); err != nil {
return nil, err
}
return cfg, nil
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, my reasoning is that since it's read-only, the whole parsing can be done during creation because entries won't change in the runtime so I can have a map of auth.Credential
ready for use and validated see the test TestReadOnlyFileStore_Create_fromInvalidConfig
. In GetCredential
and later Get` there is only access to a map; see also the comment in #850 (comment) and please tell me what you think. I can adjust my code according to your guidance but I need to understand how to do it
} | ||
|
||
// 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can refactor this in another way using generics, so that it can be reused by both config and readonly config:
Something like:
func matchAuth[V any](auths map[string]V, serverAddress string) (V, bool) {
if v, ok := auths[serverAddress]; ok {
return v, true
}
for addr, v := range auths {
if toHostname(addr) == serverAddress {
return v, true
}
}
var zero V
return zero, false
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's currently used by both stores. For readonly in LoadFromReader
where I perform parsing all entries to auth.Credential
because they won't change during the lifetime of such a config, it can be done here. There is info whether the whole config is valid and no computation is needed for getting a credential.
For file backedd config it's used in method GetCredential
. They need to be parsed and looked up on each call because they may change.
I moved it to a dedicated file helpers.go
for better visibility and adjusted some names
@@ -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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: How about: "operation not allowed: the store is read-only"?
// 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we put this in a separate file for better readability?
30e4961
to
de2c43a
Compare
Signed-off-by: Jakub Warczarek <[email protected]>
de2c43a
to
0ad13e6
Compare
What this PR does / why we need it:
Currently, struct
Config
that represents a docker configuration file can be populated only with the functionLoad(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 fromio.Reader
and supports onlyGet(...)
method according to the proposal from @Wwwsylvia described in the #850 (comment)