From 679a17d9a34dbddea3cf96f62977f5b612282f5b Mon Sep 17 00:00:00 2001 From: srinandan <13950006+srinandan@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:10:23 +0000 Subject: [PATCH 1/5] feat: add support for metadata-tokens #260 --- cmd/root.go | 27 +++++++++++++--- internal/apiclient/options.go | 1 + internal/apiclient/token.go | 60 +++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 384f8cabd..35fabfbfa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -63,8 +63,18 @@ var RootCmd = &cobra.Command{ Short: "Utility to work with Apigee APIs.", Long: "This command lets you interact with Apigee APIs.", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - apiclient.SetServiceAccount(serviceAccount) - apiclient.SetApigeeToken(accessToken) + if metadataToken && (serviceAccount != "" || accessToken != "") { + return fmt.Errorf("metadata-token cannot be used with token or account flags") + } + + if serviceAccount != "" && accessToken != "" { + return fmt.Errorf("token and account flags cannot be used together") + } + + if !metadataToken { + apiclient.SetServiceAccount(serviceAccount) + apiclient.SetApigeeToken(accessToken) + } if !disableCheck { if ok, _ := apiclient.TestAndUpdateLastCheck(); !ok { @@ -78,7 +88,11 @@ var RootCmd = &cobra.Command{ } } - _ = apiclient.SetAccessToken() + if metadataToken { + return apiclient.GetDefaultAccessToken() + } else { + _ = apiclient.SetAccessToken() + } return nil }, @@ -93,8 +107,8 @@ func Execute() { } var ( - accessToken, serviceAccount string - disableCheck, printOutput, noOutput bool + accessToken, serviceAccount string + disableCheck, printOutput, noOutput, metadataToken bool ) const ENABLED = "true" @@ -117,6 +131,9 @@ func init() { RootCmd.PersistentFlags().BoolVarP(&noOutput, "no-output", "", false, "Disable printing all statements to stdout") + RootCmd.PersistentFlags().BoolVarP(&metadataToken, "metadata-token", "", + false, "Metadata OAuth2 access token") + RootCmd.AddCommand(apis.Cmd) RootCmd.AddCommand(org.Cmd) RootCmd.AddCommand(sync.Cmd) diff --git a/internal/apiclient/options.go b/internal/apiclient/options.go index c2aa21ca3..7f32584d7 100644 --- a/internal/apiclient/options.go +++ b/internal/apiclient/options.go @@ -41,6 +41,7 @@ type ApigeeClientOptions struct { PrintOutput bool // prints output from http calls NoOutput bool // Disable all statements to stdout ProxyUrl string // use a proxy url + MetadataToken bool // use metadata outh2 token APIRate Rate // throttle api calls to Apigee } diff --git a/internal/apiclient/token.go b/internal/apiclient/token.go index 2a7b80e6a..437709e20 100644 --- a/internal/apiclient/token.go +++ b/internal/apiclient/token.go @@ -259,3 +259,63 @@ func SetAccessToken() error { } return fmt.Errorf("token expired: request a new access token or pass the service account") } + +// GetDefaultAccessToken +func GetDefaultAccessToken() (err error) { + var req *http.Request + var tokenResponse map[string]interface{} + + metadataURL := "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" + + err = GetHttpClient() + if err != nil { + return err + } + + if DryRun() { + return nil + } + + clilog.Debug.Println("Connecting to: ", metadataURL) + + req, err = http.NewRequest(http.MethodGet, metadataURL, nil) + if err != nil { + clilog.Error.Println("error in client: ", err) + return err + } + + req.Header.Set("Metadata-Flavor", "Google") + resp, err := ApigeeAPIClient.Do(req) + if err != nil { + clilog.Error.Println("error connecting: ", err) + return err + } + + if resp != nil { + defer resp.Body.Close() + } + + if resp == nil { + clilog.Error.Println("error in response: Response was null") + return fmt.Errorf("error in response: Response was null") + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + clilog.Error.Println("error in response: ", err) + return err + } else if resp.StatusCode > 399 { + clilog.Debug.Printf("status code %d, error in response: %s\n", resp.StatusCode, string(respBody)) + clilog.HttpError.Println(string(respBody)) + return errors.New(getErrorMessage(resp.StatusCode)) + } + + err = json.Unmarshal(respBody, &tokenResponse) + if err != nil { + return err + } + + SetApigeeToken(tokenResponse["access_token"].(string)) + + return nil +} From db9af6a3303fb279651f9bc39feaddc3576453d9 Mon Sep 17 00:00:00 2001 From: srinandan <13950006+srinandan@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:34:43 +0000 Subject: [PATCH 2/5] chore: fix linting issues #260 --- cmd/root.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 35fabfbfa..f53a52bc9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -90,10 +90,10 @@ var RootCmd = &cobra.Command{ if metadataToken { return apiclient.GetDefaultAccessToken() - } else { - _ = apiclient.SetAccessToken() } + _ = apiclient.SetAccessToken() + return nil }, SilenceUsage: getUsageFlag(), From 3fa19d5e6621288ac3eceb4f508d5c59ae1b8f0c Mon Sep 17 00:00:00 2001 From: srinandan <13950006+srinandan@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:37:46 -0700 Subject: [PATCH 3/5] feat: update docs for metadata token #260 --- README.md | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b79520a05..189197ee8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,10 @@ curl -L https://raw.githubusercontent.com/apigee/apigeecli/main/downloadLatest.s NOTE: The signature is not verified and the original zip is not preserved. +
+ Signature Verification +### Signature Verification To test the signature of the binary, import the gpg public key: ```sh @@ -39,6 +42,8 @@ gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: 72D1 1E3A 3B1E 9FE2 2110 EC45 A714 872F 32F3 4390 ``` +
+ ## Getting Started ### User Tokens @@ -50,23 +55,15 @@ token=$(gcloud auth print-access-token) apigeecli orgs list -t $token ``` -### Set Preferences -If you are using the same GCP project for Apigee, then consider setting up preferences so they don't have to be included in every command. Preferences are written to the `$HOME/.apigeecli` folder - -``` -project=$(gcloud config get-value project | head -n 1) - -apigeecli prefs set -o $project -``` +### Metadata OAuth2 Access Tokens -Subsequent commands can be like this: +If you are using `apigeecli` on Cloud Shell, GCE instances, Cloud Build, then you can use the metadata to get the access token -``` -token=$(gcloud auth print-access-token) -apigeecli orgs get -t $token #fetches the org details of the org set in preferences +```sh +apigeecli orgs list --metadata-token ``` -### Access Token Generation +### Access Token Generation from Service Accounts `apigeecli` can use the service account directly and obtain an access token. @@ -95,6 +92,27 @@ token=$(gcloud auth print-access-token) apigeecli token cache -t $token ``` +or +```bash +apigeecli token cache --metadata-token +``` + +## Set Preferences +If you are using the same GCP project for Apigee, then consider setting up preferences so they don't have to be included in every command. Preferences are written to the `$HOME/.apigeecli` folder + +``` +project=$(gcloud config get-value project | head -n 1) + +apigeecli prefs set -o $project +``` + +Subsequent commands can be like this: + +``` +token=$(gcloud auth print-access-token) +apigeecli orgs get -t $token #fetches the org details of the org set in preferences +``` + ## Container download The lastest container version for apigeecli can be downloaded via From e778d1ac7861c2a5b201d398c8ad9b278ec38280 Mon Sep 17 00:00:00 2001 From: srinandan <13950006+srinandan@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:40:06 -0700 Subject: [PATCH 4/5] chore: additional debug statements #260 --- internal/apiclient/token.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/apiclient/token.go b/internal/apiclient/token.go index 437709e20..cabdb4737 100644 --- a/internal/apiclient/token.go +++ b/internal/apiclient/token.go @@ -317,5 +317,16 @@ func GetDefaultAccessToken() (err error) { SetApigeeToken(tokenResponse["access_token"].(string)) + ClientPrintHttpResponse.Set(false) + defer ClientPrintHttpResponse.Set(GetCmdPrintHttpResponseSetting()) + + u, _ := url.Parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/account") + respBody, _ = HttpClient(u.String()) + clilog.Debug.Println("service token email: ", string(respBody)) + + u, _ = url.Parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes") + respBody, _ = HttpClient(u.String()) + clilog.Debug.Println("scopes: ", string(respBody)) + return nil } From 801b0911419e80049ac756cb8b592c9fe40e610d Mon Sep 17 00:00:00 2001 From: srinandan <13950006+srinandan@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:15:42 -0700 Subject: [PATCH 5/5] bug: add flavor header #260 --- internal/apiclient/token.go | 38 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/internal/apiclient/token.go b/internal/apiclient/token.go index cabdb4737..7f9deecf4 100644 --- a/internal/apiclient/token.go +++ b/internal/apiclient/token.go @@ -260,20 +260,19 @@ func SetAccessToken() error { return fmt.Errorf("token expired: request a new access token or pass the service account") } -// GetDefaultAccessToken -func GetDefaultAccessToken() (err error) { +func getMetadata(metadata string) (respBpdy []byte, err error) { var req *http.Request - var tokenResponse map[string]interface{} - metadataURL := "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" + metadataURL := fmt.Sprintf( + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/%s", metadata) err = GetHttpClient() if err != nil { - return err + return nil, err } if DryRun() { - return nil + return nil, nil } clilog.Debug.Println("Connecting to: ", metadataURL) @@ -281,14 +280,14 @@ func GetDefaultAccessToken() (err error) { req, err = http.NewRequest(http.MethodGet, metadataURL, nil) if err != nil { clilog.Error.Println("error in client: ", err) - return err + return nil, err } req.Header.Set("Metadata-Flavor", "Google") resp, err := ApigeeAPIClient.Do(req) if err != nil { clilog.Error.Println("error connecting: ", err) - return err + return nil, err } if resp != nil { @@ -297,17 +296,28 @@ func GetDefaultAccessToken() (err error) { if resp == nil { clilog.Error.Println("error in response: Response was null") - return fmt.Errorf("error in response: Response was null") + return nil, fmt.Errorf("error in response: Response was null") } respBody, err := io.ReadAll(resp.Body) if err != nil { clilog.Error.Println("error in response: ", err) - return err + return nil, err } else if resp.StatusCode > 399 { clilog.Debug.Printf("status code %d, error in response: %s\n", resp.StatusCode, string(respBody)) clilog.HttpError.Println(string(respBody)) - return errors.New(getErrorMessage(resp.StatusCode)) + return nil, errors.New(getErrorMessage(resp.StatusCode)) + } + return respBody, err +} + +// GetDefaultAccessToken +func GetDefaultAccessToken() (err error) { + var tokenResponse map[string]interface{} + + respBody, err := getMetadata("token") + if err != nil { + return err } err = json.Unmarshal(respBody, &tokenResponse) @@ -320,12 +330,10 @@ func GetDefaultAccessToken() (err error) { ClientPrintHttpResponse.Set(false) defer ClientPrintHttpResponse.Set(GetCmdPrintHttpResponseSetting()) - u, _ := url.Parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/account") - respBody, _ = HttpClient(u.String()) + respBody, _ = getMetadata("email") clilog.Debug.Println("service token email: ", string(respBody)) - u, _ = url.Parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes") - respBody, _ = HttpClient(u.String()) + respBody, _ = getMetadata("scopes") clilog.Debug.Println("scopes: ", string(respBody)) return nil