From 1f89d5d8a6976eae15bd73c9958eeaa938f36c2f Mon Sep 17 00:00:00 2001 From: Felix Kollmann Date: Wed, 12 Feb 2025 08:19:16 +0100 Subject: [PATCH 1/4] Add `goauth` binary for `GOAUTH` environment support --- cmd/goauth/main.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100755 cmd/goauth/main.go diff --git a/cmd/goauth/main.go b/cmd/goauth/main.go new file mode 100755 index 0000000..180ad07 --- /dev/null +++ b/cmd/goauth/main.go @@ -0,0 +1,94 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// 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 main + +import ( + "context" + "encoding/base64" + "flag" + "fmt" + "log" + "os" + "time" + + "github.com/GoogleCloudPlatform/artifact-registry-go-tools/pkg/auth" +) + +const help = ` +Handle Go authentication with Google Cloud Artifact Registry Go Repositories. + +Add to your GOAUTH environment variable: + + export GOAUTH="sh -c 'GOPROXY=direct go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/goauth@latest '" + +To support multiple locations, add the command multiple times to the GOAUTH variable (semicolon-separated). + +For more details, see https://pkg.go.dev/cmd/go@master#hdr-GOAUTH_environment_variable` + +func main() { + jsonKey := flag.String("json_key", "", "path to the json key of the service account used for this location. Leave empty to use the oauth token instead.") + hostPattern := flag.String("host_pattern", "%s-go.pkg.dev", "Artifact Registry server host pattern, where %s will be replaced by a location string.") + + flag.Parse() + + location := flag.Arg(0) + if location == "" { + fmt.Println(help) + return + } + + err := handleLocation(location, *jsonKey, *hostPattern) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func handleLocation(location string, jsonKeyPath string, hostPattern string) error { + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + host := fmt.Sprintf(hostPattern, location) + url := fmt.Sprintf("https://%s", host) + var authorization string + + if jsonKeyPath != "" { + key, err := auth.EncodeJsonKey(jsonKeyPath) + if err != nil { + return fmt.Errorf("failed to encode JSON key: %w", err) + } + + authorization = basicAuthHeader("_json_key_base64", key) + } else { + token, err := auth.Token(ctx) + if err != nil { + return fmt.Errorf("failed to get oauth token: %w", err) + } + + authorization = basicAuthHeader("oauth2accesstoken", token) + } + + // send the Go authentication information + fmt.Printf("%s\n\nAuthorization: %s\n\n", url, authorization) + + return nil +} + +func basicAuthHeader(username, password string) string { + a := fmt.Sprintf("%s:%s", username, password) + b := base64.StdEncoding.EncodeToString([]byte(a)) + + return fmt.Sprintf("Basic %s", b) +} From d95b5bbeb5b3a935e1afa5f240f2d201c07fe975 Mon Sep 17 00:00:00 2001 From: Felix Kollmann Date: Thu, 13 Feb 2025 07:44:29 +0100 Subject: [PATCH 2/4] Change ignoring URLs as location (for safety for the second call, but this should not happen as this requires the location to be missing, which would fail on the first call.) --- cmd/goauth/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/goauth/main.go b/cmd/goauth/main.go index 180ad07..6f8bffb 100755 --- a/cmd/goauth/main.go +++ b/cmd/goauth/main.go @@ -20,6 +20,7 @@ import ( "fmt" "log" "os" + "strings" "time" "github.com/GoogleCloudPlatform/artifact-registry-go-tools/pkg/auth" @@ -47,6 +48,10 @@ func main() { fmt.Println(help) return } + if strings.HasPrefix(location, "https://") { + log.Println("Location has to be a Google Cloud region, e.g. 'us-central1'.") + os.Exit(2) + } err := handleLocation(location, *jsonKey, *hostPattern) if err != nil { From ed04de53fd4e3e2effcd45ec1918887dd4747cfc Mon Sep 17 00:00:00 2001 From: Felix Kollmann Date: Sat, 15 Feb 2025 16:53:48 +0100 Subject: [PATCH 3/4] Change help to be written to stderr --- cmd/goauth/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/goauth/main.go b/cmd/goauth/main.go index 6f8bffb..0be9530 100755 --- a/cmd/goauth/main.go +++ b/cmd/goauth/main.go @@ -45,7 +45,7 @@ func main() { location := flag.Arg(0) if location == "" { - fmt.Println(help) + fmt.Fprintln(os.Stderr, help) return } if strings.HasPrefix(location, "https://") { From 0a36049f9a46df4b14a4075b595436f605fc8e45 Mon Sep 17 00:00:00 2001 From: Felix Kollmann Date: Sat, 15 Feb 2025 16:54:50 +0100 Subject: [PATCH 4/4] Add unit testing --- cmd/goauth/main.go | 38 +++++++++++++---------- cmd/goauth/main_test.go | 63 ++++++++++++++++++++++++++++++++++++++ cmd/goauth/test/dummy.json | 1 + 3 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 cmd/goauth/main_test.go create mode 100644 cmd/goauth/test/dummy.json diff --git a/cmd/goauth/main.go b/cmd/goauth/main.go index 0be9530..81ac6c3 100755 --- a/cmd/goauth/main.go +++ b/cmd/goauth/main.go @@ -37,9 +37,11 @@ To support multiple locations, add the command multiple times to the GOAUTH vari For more details, see https://pkg.go.dev/cmd/go@master#hdr-GOAUTH_environment_variable` +const defaultHostPattern = "%s-go.pkg.dev" + func main() { jsonKey := flag.String("json_key", "", "path to the json key of the service account used for this location. Leave empty to use the oauth token instead.") - hostPattern := flag.String("host_pattern", "%s-go.pkg.dev", "Artifact Registry server host pattern, where %s will be replaced by a location string.") + hostPattern := flag.String("host_pattern", defaultHostPattern, "Artifact Registry server host pattern, where %s will be replaced by a location string.") flag.Parse() @@ -53,42 +55,44 @@ func main() { os.Exit(2) } - err := handleLocation(location, *jsonKey, *hostPattern) + // generate the authentication header + urlLine := locationURL(location, *hostPattern) + authHeader, err := keyAuthHeader(*jsonKey) if err != nil { log.Println(err) - os.Exit(1) + os.Exit(3) } + + // send the Go authentication information + fmt.Printf("%s\n\nAuthorization: %s\n\n", urlLine, authHeader) +} + +func locationURL(location string, hostPattern string) string { + host := fmt.Sprintf(hostPattern, location) + + return fmt.Sprintf("https://%s", host) } -func handleLocation(location string, jsonKeyPath string, hostPattern string) error { +func keyAuthHeader(jsonKeyPath string) (string, error) { ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - host := fmt.Sprintf(hostPattern, location) - url := fmt.Sprintf("https://%s", host) - var authorization string - if jsonKeyPath != "" { key, err := auth.EncodeJsonKey(jsonKeyPath) if err != nil { - return fmt.Errorf("failed to encode JSON key: %w", err) + return "", fmt.Errorf("failed to encode JSON key: %w", err) } - authorization = basicAuthHeader("_json_key_base64", key) + return basicAuthHeader("_json_key_base64", key), nil } else { token, err := auth.Token(ctx) if err != nil { - return fmt.Errorf("failed to get oauth token: %w", err) + return "", fmt.Errorf("failed to get oauth token: %w", err) } - authorization = basicAuthHeader("oauth2accesstoken", token) + return basicAuthHeader("oauth2accesstoken", token), nil } - - // send the Go authentication information - fmt.Printf("%s\n\nAuthorization: %s\n\n", url, authorization) - - return nil } func basicAuthHeader(username, password string) string { diff --git a/cmd/goauth/main_test.go b/cmd/goauth/main_test.go new file mode 100644 index 0000000..da2d2f9 --- /dev/null +++ b/cmd/goauth/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/base64" + "strings" + "testing" +) + +func TestLocationURL(t *testing.T) { + url := locationURL("us-central1", defaultHostPattern) + + if url != "https://us-central1-go.pkg.dev" { + t.Fatalf("unexpected url: %s", url) + } +} + +func TestAuthHeader_DefaultCredentials(t *testing.T) { + header, err := keyAuthHeader("") + if err != nil { + t.Fatal(err) + } + + t.Logf("header: %s", header) + + if !strings.HasPrefix(header, "Basic ") { + t.Fatal(err) + } + + decoded, err := base64.StdEncoding.DecodeString(header[6:]) + if err != nil { + t.Fatal(err) + } + + t.Logf("decoded: %s", decoded) + + if !strings.HasPrefix(string(decoded), "oauth2accesstoken:") { + t.Fatal(err) + } +} + +func TestAuthHeader_JSONKey(t *testing.T) { + header, err := keyAuthHeader("./test/dummy.json") + if err != nil { + t.Fatal(err) + } + + t.Logf("header: %s", header) + + if !strings.HasPrefix(header, "Basic ") { + t.Fatal(err) + } + + decoded, err := base64.StdEncoding.DecodeString(header[6:]) + if err != nil { + t.Fatal(err) + } + + t.Logf("decoded: %s", decoded) + + if !strings.HasPrefix(string(decoded), "_json_key_base64:") { + t.Fatal(err) + } +} diff --git a/cmd/goauth/test/dummy.json b/cmd/goauth/test/dummy.json new file mode 100644 index 0000000..eb91b54 --- /dev/null +++ b/cmd/goauth/test/dummy.json @@ -0,0 +1 @@ +{ "comment": "the content of this file is irrelevant" } \ No newline at end of file