oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/registry/remote/credentials/store.go (about) 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 supports reading, saving, and removing credentials from 17 // Docker configuration files and external credential stores that follow 18 // the Docker credential helper protocol. 19 // 20 // Reference: https://docs.docker.com/engine/reference/commandline/login/#credential-stores 21 package credentials 22 23 import ( 24 "context" 25 "fmt" 26 "os" 27 "path/filepath" 28 29 "oras.land/oras-go/v2/internal/syncutil" 30 "oras.land/oras-go/v2/registry/remote/auth" 31 "oras.land/oras-go/v2/registry/remote/credentials/internal/config" 32 ) 33 34 const ( 35 dockerConfigDirEnv = "DOCKER_CONFIG" 36 dockerConfigFileDir = ".docker" 37 dockerConfigFileName = "config.json" 38 ) 39 40 // Store is the interface that any credentials store must implement. 41 type Store interface { 42 // Get retrieves credentials from the store for the given server address. 43 Get(ctx context.Context, serverAddress string) (auth.Credential, error) 44 // Put saves credentials into the store for the given server address. 45 Put(ctx context.Context, serverAddress string, cred auth.Credential) error 46 // Delete removes credentials from the store for the given server address. 47 Delete(ctx context.Context, serverAddress string) error 48 } 49 50 // DynamicStore dynamically determines which store to use based on the settings 51 // in the config file. 52 type DynamicStore struct { 53 config *config.Config 54 options StoreOptions 55 detectedCredsStore string 56 setCredsStoreOnce syncutil.OnceOrRetry 57 } 58 59 // StoreOptions provides options for NewStore. 60 type StoreOptions struct { 61 // AllowPlaintextPut allows saving credentials in plaintext in the config 62 // file. 63 // - If AllowPlaintextPut is set to false (default value), Put() will 64 // return an error when native store is not available. 65 // - If AllowPlaintextPut is set to true, Put() will save credentials in 66 // plaintext in the config file when native store is not available. 67 AllowPlaintextPut bool 68 69 // DetectDefaultNativeStore enables detecting the platform-default native 70 // credentials store when the config file has no authentication information. 71 // 72 // If DetectDefaultNativeStore is set to true, the store will detect and set 73 // the default native credentials store in the "credsStore" field of the 74 // config file. 75 // - Windows: "wincred" 76 // - Linux: "pass" or "secretservice" 77 // - macOS: "osxkeychain" 78 // 79 // References: 80 // - https://docs.docker.com/engine/reference/commandline/login/#credentials-store 81 // - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties 82 DetectDefaultNativeStore bool 83 } 84 85 // NewStore returns a Store based on the given configuration file. 86 // 87 // For Get(), Put() and Delete(), the returned Store will dynamically determine 88 // which underlying credentials store to use for the given server address. 89 // The underlying credentials store is determined in the following order: 90 // 1. Native server-specific credential helper 91 // 2. Native credentials store 92 // 3. The plain-text config file itself 93 // 94 // References: 95 // - https://docs.docker.com/engine/reference/commandline/login/#credentials-store 96 // - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties 97 func NewStore(configPath string, opts StoreOptions) (*DynamicStore, error) { 98 cfg, err := config.Load(configPath) 99 if err != nil { 100 return nil, err 101 } 102 ds := &DynamicStore{ 103 config: cfg, 104 options: opts, 105 } 106 if opts.DetectDefaultNativeStore && !cfg.IsAuthConfigured() { 107 // no authentication configured, detect the default credentials store 108 ds.detectedCredsStore = getDefaultHelperSuffix() 109 } 110 return ds, nil 111 } 112 113 // NewStoreFromDocker returns a Store based on the default docker config file. 114 // - If the $DOCKER_CONFIG environment variable is set, 115 // $DOCKER_CONFIG/config.json will be used. 116 // - Otherwise, the default location $HOME/.docker/config.json will be used. 117 // 118 // NewStoreFromDocker internally calls [NewStore]. 119 // 120 // References: 121 // - https://docs.docker.com/engine/reference/commandline/cli/#configuration-files 122 // - https://docs.docker.com/engine/reference/commandline/cli/#change-the-docker-directory 123 func NewStoreFromDocker(opt StoreOptions) (*DynamicStore, error) { 124 configPath, err := getDockerConfigPath() 125 if err != nil { 126 return nil, err 127 } 128 return NewStore(configPath, opt) 129 } 130 131 // Get retrieves credentials from the store for the given server address. 132 func (ds *DynamicStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { 133 return ds.getStore(serverAddress).Get(ctx, serverAddress) 134 } 135 136 // Put saves credentials into the store for the given server address. 137 // Put returns ErrPlaintextPutDisabled if native store is not available and 138 // [StoreOptions].AllowPlaintextPut is set to false. 139 func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { 140 if err := ds.getStore(serverAddress).Put(ctx, serverAddress, cred); err != nil { 141 return err 142 } 143 // save the detected creds store back to the config file on first put 144 return ds.setCredsStoreOnce.Do(func() error { 145 if ds.detectedCredsStore != "" { 146 if err := ds.config.SetCredentialsStore(ds.detectedCredsStore); err != nil { 147 return fmt.Errorf("failed to set credsStore: %w", err) 148 } 149 } 150 return nil 151 }) 152 } 153 154 // Delete removes credentials from the store for the given server address. 155 func (ds *DynamicStore) Delete(ctx context.Context, serverAddress string) error { 156 return ds.getStore(serverAddress).Delete(ctx, serverAddress) 157 } 158 159 // IsAuthConfigured returns whether there is authentication configured in the 160 // config file or not. 161 // 162 // IsAuthConfigured returns true when: 163 // - The "credsStore" field is not empty 164 // - Or the "credHelpers" field is not empty 165 // - Or there is any entry in the "auths" field 166 func (ds *DynamicStore) IsAuthConfigured() bool { 167 return ds.config.IsAuthConfigured() 168 } 169 170 // ConfigPath returns the path to the config file. 171 func (ds *DynamicStore) ConfigPath() string { 172 return ds.config.Path() 173 } 174 175 // getHelperSuffix returns the credential helper suffix for the given server 176 // address. 177 func (ds *DynamicStore) getHelperSuffix(serverAddress string) string { 178 // 1. Look for a server-specific credential helper first 179 if helper := ds.config.GetCredentialHelper(serverAddress); helper != "" { 180 return helper 181 } 182 // 2. Then look for the configured native store 183 if credsStore := ds.config.CredentialsStore(); credsStore != "" { 184 return credsStore 185 } 186 // 3. Use the detected default store 187 return ds.detectedCredsStore 188 } 189 190 // getStore returns a store for the given server address. 191 func (ds *DynamicStore) getStore(serverAddress string) Store { 192 if helper := ds.getHelperSuffix(serverAddress); helper != "" { 193 return NewNativeStore(helper) 194 } 195 196 fs := newFileStore(ds.config) 197 fs.DisablePut = !ds.options.AllowPlaintextPut 198 return fs 199 } 200 201 // getDockerConfigPath returns the path to the default docker config file. 202 func getDockerConfigPath() (string, error) { 203 // first try the environment variable 204 configDir := os.Getenv(dockerConfigDirEnv) 205 if configDir == "" { 206 // then try home directory 207 homeDir, err := os.UserHomeDir() 208 if err != nil { 209 return "", fmt.Errorf("failed to get user home directory: %w", err) 210 } 211 configDir = filepath.Join(homeDir, dockerConfigFileDir) 212 } 213 return filepath.Join(configDir, dockerConfigFileName), nil 214 } 215 216 // storeWithFallbacks is a store that has multiple fallback stores. 217 type storeWithFallbacks struct { 218 stores []Store 219 } 220 221 // NewStoreWithFallbacks returns a new store based on the given stores. 222 // - Get() searches the primary and the fallback stores 223 // for the credentials and returns when it finds the 224 // credentials in any of the stores. 225 // - Put() saves the credentials into the primary store. 226 // - Delete() deletes the credentials from the primary store. 227 func NewStoreWithFallbacks(primary Store, fallbacks ...Store) Store { 228 if len(fallbacks) == 0 { 229 return primary 230 } 231 return &storeWithFallbacks{ 232 stores: append([]Store{primary}, fallbacks...), 233 } 234 } 235 236 // Get retrieves credentials from the StoreWithFallbacks for the given server. 237 // It searches the primary and the fallback stores for the credentials of serverAddress 238 // and returns when it finds the credentials in any of the stores. 239 func (sf *storeWithFallbacks) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { 240 for _, s := range sf.stores { 241 cred, err := s.Get(ctx, serverAddress) 242 if err != nil { 243 return auth.EmptyCredential, err 244 } 245 if cred != auth.EmptyCredential { 246 return cred, nil 247 } 248 } 249 return auth.EmptyCredential, nil 250 } 251 252 // Put saves credentials into the StoreWithFallbacks. It puts 253 // the credentials into the primary store. 254 func (sf *storeWithFallbacks) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { 255 return sf.stores[0].Put(ctx, serverAddress, cred) 256 } 257 258 // Delete removes credentials from the StoreWithFallbacks for the given server. 259 // It deletes the credentials from the primary store. 260 func (sf *storeWithFallbacks) Delete(ctx context.Context, serverAddress string) error { 261 return sf.stores[0].Delete(ctx, serverAddress) 262 }