github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/gce/credentials.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package gce
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils/v3"
    14  
    15  	"github.com/juju/juju/cloud"
    16  	"github.com/juju/juju/environs"
    17  	"github.com/juju/juju/provider/gce/google"
    18  )
    19  
    20  const (
    21  	credAttrPrivateKey  = "private-key"
    22  	credAttrClientID    = "client-id"
    23  	credAttrClientEmail = "client-email"
    24  	credAttrProjectID   = "project-id"
    25  
    26  	// The contents of the file for "jsonfile" auth-type.
    27  	credAttrFile = "file"
    28  )
    29  
    30  type environProviderCredentials struct{}
    31  
    32  // CredentialSchemas is part of the environs.ProviderCredentials interface.
    33  func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema {
    34  	return map[cloud.AuthType]cloud.CredentialSchema{
    35  		cloud.OAuth2AuthType: {{
    36  			Name:           credAttrClientID,
    37  			CredentialAttr: cloud.CredentialAttr{Description: "client ID"},
    38  		}, {
    39  			Name:           credAttrClientEmail,
    40  			CredentialAttr: cloud.CredentialAttr{Description: "client e-mail address"},
    41  		}, {
    42  			Name: credAttrPrivateKey,
    43  			CredentialAttr: cloud.CredentialAttr{
    44  				Description: "client secret",
    45  				Hidden:      true,
    46  			},
    47  		}, {
    48  			Name:           credAttrProjectID,
    49  			CredentialAttr: cloud.CredentialAttr{Description: "project ID"},
    50  		}},
    51  		cloud.JSONFileAuthType: {{
    52  			Name: credAttrFile,
    53  			CredentialAttr: cloud.CredentialAttr{
    54  				Description: "path to the .json file containing a service account key for your project\n(detailed instructions available at https://discourse.charmhub.io/t/1508).\nPath",
    55  				FilePath:    true,
    56  			},
    57  		}},
    58  	}
    59  }
    60  
    61  // DetectCredentials is part of the environs.ProviderCredentials interface.
    62  func (environProviderCredentials) DetectCredentials(cloudName string) (*cloud.CloudCredential, error) {
    63  	// Google recommends credentials in a json file:
    64  	// 1. whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable.
    65  	// 2. whose location is known to the gcloud command-line tool.
    66  	//   On *nix, this is $HOME/.config/gcloud/application_default_credentials.json.
    67  
    68  	validatePath := func(possibleFilePath string) string {
    69  		if possibleFilePath == "" {
    70  			return ""
    71  		}
    72  		fi, err := os.Stat(possibleFilePath)
    73  		if err != nil || fi.IsDir() {
    74  			return ""
    75  		}
    76  		return possibleFilePath
    77  	}
    78  
    79  	possibleFilePath := validatePath(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
    80  	if possibleFilePath == "" {
    81  		possibleFilePath = validatePath(wellKnownCredentialsFile())
    82  	}
    83  	if possibleFilePath == "" {
    84  		return nil, errors.NotFoundf("gce credentials")
    85  	}
    86  
    87  	authFile, err := os.Open(possibleFilePath)
    88  	if err != nil {
    89  		return nil, errors.Trace(err)
    90  	}
    91  	defer authFile.Close()
    92  
    93  	parsedCred, err := parseJSONAuthFile(authFile)
    94  	if err != nil {
    95  		return nil, errors.Annotatef(err, "invalid json credential file %s", possibleFilePath)
    96  	}
    97  
    98  	user, err := utils.LocalUsername()
    99  	if err != nil {
   100  		return nil, errors.Trace(err)
   101  	}
   102  	cred := cloud.NewCredential(cloud.JSONFileAuthType, map[string]string{
   103  		"file": possibleFilePath,
   104  	})
   105  	credName := parsedCred.Attributes()[credAttrClientEmail]
   106  	if credName == "" {
   107  		credName = parsedCred.Attributes()[credAttrClientID]
   108  	}
   109  	cred.Label = fmt.Sprintf("google credential %q", credName)
   110  	return &cloud.CloudCredential{
   111  		DefaultRegion: os.Getenv("CLOUDSDK_COMPUTE_REGION"),
   112  		AuthCredentials: map[string]cloud.Credential{
   113  			user: cred,
   114  		}}, nil
   115  }
   116  
   117  func wellKnownCredentialsFile() string {
   118  	const f = "application_default_credentials.json"
   119  	return filepath.Join(utils.Home(), ".config", "gcloud", f)
   120  }
   121  
   122  // parseJSONAuthFile parses a file, and extracts the OAuth2 credentials within.
   123  func parseJSONAuthFile(r io.Reader) (cloud.Credential, error) {
   124  	creds, err := google.ParseJSONKey(r)
   125  	if err != nil {
   126  		return cloud.Credential{}, errors.Trace(err)
   127  	}
   128  	return cloud.NewCredential(cloud.OAuth2AuthType, map[string]string{
   129  		credAttrProjectID:   creds.ProjectID,
   130  		credAttrClientID:    creds.ClientID,
   131  		credAttrClientEmail: creds.ClientEmail,
   132  		credAttrPrivateKey:  string(creds.PrivateKey),
   133  	}), nil
   134  }
   135  
   136  // FinalizeCredential is part of the environs.ProviderCredentials interface.
   137  func (environProviderCredentials) FinalizeCredential(_ environs.FinalizeCredentialContext, args environs.FinalizeCredentialParams) (*cloud.Credential, error) {
   138  	return &args.Credential, nil
   139  }