store/store.go (158 lines of code) (raw):

// 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) }