From 7c5046d555ee65074f4c8e05126bc99db8fae5fd Mon Sep 17 00:00:00 2001 From: Patrick Meredith Date: Wed, 24 Jul 2024 15:43:17 -0400 Subject: [PATCH] GODRIVER-2806: Implement automatic GCP token acquisition (#1708) Co-authored-by: Matt Dale <9760375+matthewdale@users.noreply.github.com> --- .evergreen/config.yml | 51 +++++++++++++++++++++++++++++++++++++ cmd/testoidcauth/main.go | 22 ++++++++++++++++ x/mongo/driver/auth/oidc.go | 42 ++++++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 327d616c5b..ce64932963 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -2000,6 +2000,31 @@ tasks: export AZUREOIDC_TEST_CMD="PROJECT_DIRECTORY='.' OIDC_ENV=azure OIDC=oidc ./etc/run-oidc-test.sh ./test" bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + - name: "oidc-auth-test-gcp-latest" + commands: + - command: shell.exec + params: + working_dir: src/go.mongodb.org/mongo-driver + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-go-driver.tar.gz + # we need to statically link libc to avoid the situation where the VM has a different + # version of libc + go build -tags osusergo,netgo -ldflags '-w -extldflags "-static -lgcc -lc"' -o test ./cmd/testoidcauth/main.go + rm "$GCPOIDC_DRIVERS_TAR_FILE" || true + tar -cf $GCPOIDC_DRIVERS_TAR_FILE ./test + tar -uf $GCPOIDC_DRIVERS_TAR_FILE ./etc + rm "$GCPOIDC_DRIVERS_TAR_FILE".gz || true + gzip $GCPOIDC_DRIVERS_TAR_FILE + export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-go-driver.tar.gz + # Define the command to run on the gcp VM. + # Ensure that we source the environment file created for us, set up any other variables we need, + # and then run our test suite on the vm. + export GCPOIDC_TEST_CMD="PROJECT_DIRECTORY='.' OIDC_ENV=gcp OIDC=oidc ./etc/run-oidc-test.sh ./test" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh + - name: "test-search-index" commands: - func: "bootstrap-mongo-orchestration" @@ -2342,6 +2367,30 @@ task_groups: tasks: - oidc-auth-test-azure-latest + - name: testgcpoidc_task_group + setup_group: + - func: fetch-source + - func: prepare-resources + - func: fix-absolute-paths + - func: make-files-executable + - command: subprocess.exec + params: + binary: bash + env: + AZUREOIDC_VMNAME_PREFIX: "GO_DRIVER" + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/setup.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest + - name: test-aws-lambda-task-group setup_group: - func: fetch-source @@ -2693,3 +2742,5 @@ buildvariants: batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - name: testazureoidc_task_group batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README + - name: testgcpoidc_task_group + batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README diff --git a/cmd/testoidcauth/main.go b/cmd/testoidcauth/main.go index 9fb12209cd..060f464355 100644 --- a/cmd/testoidcauth/main.go +++ b/cmd/testoidcauth/main.go @@ -91,6 +91,8 @@ func main() { case "azure": aux("machine_5_1_azureWithNoUsername", machine51azureWithNoUsername) aux("machine_5_2_azureWithNoUsername", machine52azureWithBadUsername) + case "gcp": + aux("machine_6_1_gcpWithNoUsername", machine61gcpWithNoUsername) default: log.Fatal("Unknown OIDC_ENV: ", env) } @@ -736,3 +738,23 @@ func machine52azureWithBadUsername() error { } return nil } + +func machine61gcpWithNoUsername() error { + opts := options.Client().ApplyURI(uriSingle) + if opts == nil || opts.Auth == nil { + return fmt.Errorf("machine_6_1: failed parsing uri: %q", uriSingle) + } + client, err := mongo.Connect(context.Background(), opts) + if err != nil { + return fmt.Errorf("machine_6_1: failed connecting client: %v", err) + } + defer client.Disconnect(context.Background()) + + coll := client.Database("test").Collection("test") + + _, err = coll.Find(context.Background(), bson.D{}) + if err != nil { + return fmt.Errorf("machine_6_1: failed executing Find: %v", err) + } + return nil +} diff --git a/x/mongo/driver/auth/oidc.go b/x/mongo/driver/auth/oidc.go index 3153ee8e89..fe0584eb14 100644 --- a/x/mongo/driver/auth/oidc.go +++ b/x/mongo/driver/auth/oidc.go @@ -10,6 +10,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/url" "strings" @@ -174,12 +175,12 @@ func (oa *OIDCAuthenticator) providerCallback() (OIDCCallback, error) { return nil, newAuthError(fmt.Sprintf("%q must be specified for Azure OIDC", resourceProp), nil) } return getAzureOIDCCallback(oa.userName, resource, oa.httpClient), nil - // TODO GODRIVER-2806: Automatic token acquisition for GCP Identity Provider - // This is here just to pass the linter, it will be fixed in one of the above tickets. case gcpEnvironmentValue: - return func(ctx context.Context, args *OIDCArgs) (*OIDCCredential, error) { - return nil, fmt.Errorf("automatic token acquisition for %q not implemented yet", env) - }, fmt.Errorf("automatic token acquisition for %q not implemented yet", env) + resource, ok := oa.AuthMechanismProperties[resourceProp] + if !ok { + return nil, newAuthError(fmt.Sprintf("%q must be specified for GCP OIDC", resourceProp), nil) + } + return getGCPOIDCCallback(resource, oa.httpClient), nil } return nil, fmt.Errorf("%q %q not supported for MONGODB-OIDC", environmentProp, env) @@ -228,6 +229,37 @@ func getAzureOIDCCallback(clientID string, resource string, httpClient *http.Cli } } +// getGCPOIDCCallback returns the callback for the GCP Identity Provider. +func getGCPOIDCCallback(resource string, httpClient *http.Client) OIDCCallback { + // return the callback parameterized by the clientID and resource, also passing in the user + // configured httpClient. + return func(ctx context.Context, args *OIDCArgs) (*OIDCCredential, error) { + resource = url.QueryEscape(resource) + uri := fmt.Sprintf("http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s", resource) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, newAuthError("error creating http request to GCP Identity Provider", err) + } + req.Header.Add("Metadata-Flavor", "Google") + resp, err := httpClient.Do(req) + if err != nil { + return nil, newAuthError("error getting access token from GCP Identity Provider", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, newAuthError(fmt.Sprintf("failed to get a valid response from GCP Identity Provider, http code: %d", resp.StatusCode), nil) + } + accessToken, err := io.ReadAll(resp.Body) + if err != nil { + return nil, newAuthError("failed parsing reading response from GCP Identity Provider", err) + } + return &OIDCCredential{ + AccessToken: string(accessToken), + ExpiresAt: nil, + }, nil + } +} + func (oa *OIDCAuthenticator) getAccessToken( ctx context.Context, conn driver.Connection,