| // Copyright 2016 Google, Inc. |
| // |
| // 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 store implements a credential store that is capable of storing both |
| plain Docker credentials as well as GCR access and refresh tokens. |
| */ |
| package store |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "github.com/GoogleCloudPlatform/docker-credential-gcr/v2/config" |
| "github.com/GoogleCloudPlatform/docker-credential-gcr/v2/util" |
| "github.com/docker/docker-credential-helpers/credentials" |
| "golang.org/x/oauth2" |
| "golang.org/x/oauth2/google" |
| ) |
| |
| const ( |
| credentialStoreEnvVar = "DOCKER_CREDENTIAL_GCR_STORE" |
| credentialStoreFilename = "docker_credentials.json" |
| ) |
| |
| type tokens struct { |
| AccessToken string `json:"access_token"` |
| RefreshToken string `json:"refresh_token"` |
| TokenExpiry *time.Time `json:"token_expiry"` |
| } |
| |
| type dockerCredentials struct { |
| GCRCreds *tokens `json:"gcrCreds,omitempty"` |
| } |
| |
| // A GCRAuth provides access to tokens from a prior login. |
| type GCRAuth struct { |
| conf *oauth2.Config |
| initialToken *oauth2.Token |
| } |
| |
| // TokenSource returns an oauth2.TokenSource that retrieve tokens from |
| // GCR credentials using the provided context. |
| // It will returns the current access token stored in the credentials, |
| // and refresh it when it expires, but it won't update the credentials |
| // with the new access token. |
| func (a *GCRAuth) TokenSource(ctx context.Context) oauth2.TokenSource { |
| return a.conf.TokenSource(ctx, a.initialToken) |
| } |
| |
| // GCRCredStore describes the interface for a store capable of storing both |
| // GCR's credentials (OAuth2 access/refresh tokens) as well as generic |
| // Docker credentials. |
| type GCRCredStore interface { |
| GetGCRAuth() (*GCRAuth, error) |
| SetGCRAuth(tok *oauth2.Token) error |
| DeleteGCRAuth() error |
| } |
| |
| type credStore struct { |
| credentialPath string |
| } |
| |
| // DefaultGCRCredStore returns a GCRCredStore which is backed by a file. |
| func DefaultGCRCredStore() (GCRCredStore, error) { |
| path, err := dockerCredentialPath() |
| return &credStore{ |
| credentialPath: path, |
| }, err |
| } |
| |
| // NewGCRCredStore returns a GCRCredStore which is backed by the given file. |
| func NewGCRCredStore(path string) GCRCredStore { |
| return &credStore{ |
| credentialPath: path, |
| } |
| } |
| |
| // GetGCRAuth creates an GCRAuth for the currently signed-in account. |
| func (s *credStore) GetGCRAuth() (*GCRAuth, error) { |
| creds, err := s.loadDockerCredentials() |
| if err != nil { |
| if os.IsNotExist(err) { |
| // No file, no credentials. |
| return nil, credentials.NewErrCredentialsNotFound() |
| } |
| return nil, err |
| } |
| |
| if creds.GCRCreds == nil { |
| return nil, errors.New("GCR Credentials not present in store") |
| } |
| |
| var expiry time.Time |
| if creds.GCRCreds.TokenExpiry != nil { |
| expiry = *creds.GCRCreds.TokenExpiry |
| } |
| |
| return &GCRAuth{ |
| conf: &oauth2.Config{ |
| ClientID: config.GCRCredHelperClientID, |
| ClientSecret: config.GCRCredHelperClientNotSoSecret, |
| Scopes: config.GCRScopes, |
| Endpoint: google.Endpoint, |
| RedirectURL: "oob", |
| }, |
| initialToken: &oauth2.Token{ |
| AccessToken: creds.GCRCreds.AccessToken, |
| RefreshToken: creds.GCRCreds.RefreshToken, |
| Expiry: expiry, |
| }, |
| }, nil |
| } |
| |
| // SetGCRAuth sets the stored GCR credentials. |
| func (s *credStore) SetGCRAuth(tok *oauth2.Token) error { |
| creds, err := s.loadDockerCredentials() |
| if err != nil { |
| // It's OK if we couldn't read any credentials, |
| // making a new file. |
| creds = &dockerCredentials{} |
| } |
| |
| creds.GCRCreds = &tokens{ |
| AccessToken: tok.AccessToken, |
| RefreshToken: tok.RefreshToken, |
| TokenExpiry: &tok.Expiry, |
| } |
| |
| return s.setDockerCredentials(creds) |
| } |
| |
| // DeleteGCRAuth deletes the stored GCR credentials. |
| func (s *credStore) DeleteGCRAuth() error { |
| creds, err := s.loadDockerCredentials() |
| if err != nil { |
| if os.IsNotExist(err) { |
| // No file, no credentials. |
| return nil |
| } |
| return err |
| } |
| |
| // Optimization: only perform a 'set' if necessary |
| if creds.GCRCreds != nil { |
| creds.GCRCreds = nil |
| return s.setDockerCredentials(creds) |
| } |
| return nil |
| } |
| |
| func (s *credStore) createCredentialFile() (*os.File, error) { |
| // create the gcloud config dir, if it doesnt exist |
| if err := os.MkdirAll(filepath.Dir(s.credentialPath), 0777); err != nil { |
| return nil, err |
| } |
| // create the credential file, or truncate (clear) it if it exists |
| f, err := os.Create(s.credentialPath) |
| os.Chmod(s.credentialPath, 0600) |
| if err != nil { |
| return nil, authErr("failed to create credential file", err) |
| } |
| return f, nil |
| } |
| |
| func (s *credStore) loadDockerCredentials() (*dockerCredentials, error) { |
| path := s.credentialPath |
| f, err := os.Open(path) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| |
| var creds dockerCredentials |
| if err := json.NewDecoder(f).Decode(&creds); err != nil { |
| return nil, authErr("failed to decode credentials from "+path, err) |
| } |
| |
| return &creds, nil |
| } |
| |
| func (s *credStore) setDockerCredentials(creds *dockerCredentials) error { |
| f, err := s.createCredentialFile() |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| return json.NewEncoder(f).Encode(creds) |
| } |
| |
| // dockerCredentialPath returns the full path of our Docker credential store. |
| func dockerCredentialPath() (string, error) { |
| if path := os.Getenv(credentialStoreEnvVar); strings.TrimSpace(path) != "" { |
| return path, nil |
| } |
| |
| configPath, err := util.SdkConfigPath() |
| if err != nil { |
| return "", authErr("couldn't construct config path", err) |
| } |
| return filepath.Join(configPath, credentialStoreFilename), nil |
| } |
| |
| func authErr(message string, err error) error { |
| if err == nil { |
| return fmt.Errorf("docker-credential-gcr/store: %s", message) |
| } |
| return fmt.Errorf("docker-credential-gcr/store: %s: %v", message, err) |
| } |