diff --git a/.evergreen/config.yml b/.evergreen/config.yml index ec23858817..327d616c5b 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -365,7 +365,7 @@ functions: script: | ${PREPARE_SHELL} export OIDC="oidc" - bash ${PROJECT_DIRECTORY}/etc/run-oidc-test.sh + bash ${PROJECT_DIRECTORY}/etc/run-oidc-test.sh 'make -s evg-test-oidc-auth' run-make: - command: shell.exec @@ -1975,6 +1975,31 @@ tasks: commands: - func: "run-oidc-auth-test-with-test-credentials" + - name: "oidc-auth-test-azure-latest" + commands: + - command: shell.exec + params: + working_dir: src/go.mongodb.org/mongo-driver + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export AZUREOIDC_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 "$AZUREOIDC_DRIVERS_TAR_FILE" || true + tar -cf $AZUREOIDC_DRIVERS_TAR_FILE ./test + tar -uf $AZUREOIDC_DRIVERS_TAR_FILE ./etc + rm "$AZUREOIDC_DRIVERS_TAR_FILE".gz || true + gzip $AZUREOIDC_DRIVERS_TAR_FILE + export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-go-driver.tar.gz + # Define the command to run on the azure 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 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: "test-search-index" commands: - func: "bootstrap-mongo-orchestration" @@ -2293,6 +2318,30 @@ task_groups: tasks: - oidc-auth-test-latest + - name: testazureoidc_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/azure/create-and-setup-vm.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/delete-vm.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-azure-latest + - name: test-aws-lambda-task-group setup_group: - func: fetch-source @@ -2642,3 +2691,5 @@ buildvariants: tasks: - name: testoidc_task_group 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 diff --git a/cmd/testoidcauth/main.go b/cmd/testoidcauth/main.go index 82e95f1db1..9fb12209cd 100644 --- a/cmd/testoidcauth/main.go +++ b/cmd/testoidcauth/main.go @@ -74,17 +74,26 @@ func main() { fmt.Println("...Ok") } } - aux("machine_1_1_callbackIsCalled", machine11callbackIsCalled) - aux("machine_1_2_callbackIsCalledOnlyOneForMultipleConnections", machine12callbackIsCalledOnlyOneForMultipleConnections) - aux("machine_2_1_validCallbackInputs", machine21validCallbackInputs) - aux("machine_2_3_oidcCallbackReturnMissingData", machine23oidcCallbackReturnMissingData) - aux("machine_2_4_invalidClientConfigurationWithCallback", machine24invalidClientConfigurationWithCallback) - aux("machine_3_1_failureWithCachedTokensFetchANewTokenAndRetryAuth", machine31failureWithCachedTokensFetchANewTokenAndRetryAuth) - aux("machine_3_2_authFailuresWithoutCachedTokensReturnsAnError", machine32authFailuresWithoutCachedTokensReturnsAnError) - aux("machine_3_3_UnexpectedErrorCodeDoesNotClearTheCache", machine33UnexpectedErrorCodeDoesNotClearTheCache) - aux("machine_4_1_reauthenticationSucceeds", machine41ReauthenticationSucceeds) - aux("machine_4_2_readCommandsFailIfReauthenticationFails", machine42ReadCommandsFailIfReauthenticationFails) - aux("machine_4_3_writeCommandsFailIfReauthenticationFails", machine43WriteCommandsFailIfReauthenticationFails) + env := os.Getenv("OIDC_ENV") + switch env { + case "": + aux("machine_1_1_callbackIsCalled", machine11callbackIsCalled) + aux("machine_1_2_callbackIsCalledOnlyOneForMultipleConnections", machine12callbackIsCalledOnlyOneForMultipleConnections) + aux("machine_2_1_validCallbackInputs", machine21validCallbackInputs) + aux("machine_2_3_oidcCallbackReturnMissingData", machine23oidcCallbackReturnMissingData) + aux("machine_2_4_invalidClientConfigurationWithCallback", machine24invalidClientConfigurationWithCallback) + aux("machine_3_1_failureWithCachedTokensFetchANewTokenAndRetryAuth", machine31failureWithCachedTokensFetchANewTokenAndRetryAuth) + aux("machine_3_2_authFailuresWithoutCachedTokensReturnsAnError", machine32authFailuresWithoutCachedTokensReturnsAnError) + aux("machine_3_3_UnexpectedErrorCodeDoesNotClearTheCache", machine33UnexpectedErrorCodeDoesNotClearTheCache) + aux("machine_4_1_reauthenticationSucceeds", machine41ReauthenticationSucceeds) + aux("machine_4_2_readCommandsFailIfReauthenticationFails", machine42ReadCommandsFailIfReauthenticationFails) + aux("machine_4_3_writeCommandsFailIfReauthenticationFails", machine43WriteCommandsFailIfReauthenticationFails) + case "azure": + aux("machine_5_1_azureWithNoUsername", machine51azureWithNoUsername) + aux("machine_5_2_azureWithNoUsername", machine52azureWithBadUsername) + default: + log.Fatal("Unknown OIDC_ENV: ", env) + } if hasError { log.Fatal("One or more tests failed") } @@ -686,3 +695,44 @@ func machine43WriteCommandsFailIfReauthenticationFails() error { } return callbackFailed } + +func machine51azureWithNoUsername() error { + opts := options.Client().ApplyURI(uriSingle) + if opts == nil || opts.Auth == nil { + return fmt.Errorf("machine_5_1: failed parsing uri: %q", uriSingle) + } + client, err := mongo.Connect(context.Background(), opts) + if err != nil { + return fmt.Errorf("machine_5_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_5_1: failed executing Find: %v", err) + } + return nil +} + +func machine52azureWithBadUsername() error { + opts := options.Client().ApplyURI(uriSingle) + if opts == nil || opts.Auth == nil { + return fmt.Errorf("machine_5_2: failed parsing uri: %q", uriSingle) + } + opts.Auth.Username = "bad" + client, err := mongo.Connect(context.Background(), opts) + if err != nil { + return fmt.Errorf("machine_5_2: 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_5_2: Find succeeded when it should fail") + } + return nil +} diff --git a/etc/run-oidc-test.sh b/etc/run-oidc-test.sh index bc5eb99758..4548a124a1 100644 --- a/etc/run-oidc-test.sh +++ b/etc/run-oidc-test.sh @@ -30,4 +30,4 @@ export TEST_AUTH_OIDC=1 export COVERAGE=1 export AUTH="auth" -make -s evg-test-oidc-auth +$1 diff --git a/x/mongo/driver/auth/oidc.go b/x/mongo/driver/auth/oidc.go index 91748598d3..3153ee8e89 100644 --- a/x/mongo/driver/auth/oidc.go +++ b/x/mongo/driver/auth/oidc.go @@ -8,8 +8,10 @@ package auth import ( "context" + "encoding/json" "fmt" "net/http" + "net/url" "strings" "sync" "time" @@ -166,10 +168,15 @@ func (oa *OIDCAuthenticator) providerCallback() (OIDCCallback, error) { } switch env { - // TODO GODRIVER-2728: Automatic token acquisition for Azure Identity Provider + case azureEnvironmentValue: + resource, ok := oa.AuthMechanismProperties[resourceProp] + if !ok { + 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 azureEnvironmentValue, gcpEnvironmentValue: + 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) @@ -178,6 +185,49 @@ func (oa *OIDCAuthenticator) providerCallback() (OIDCCallback, error) { return nil, fmt.Errorf("%q %q not supported for MONGODB-OIDC", environmentProp, env) } +// getAzureOIDCCallback returns the callback for the Azure Identity Provider. +func getAzureOIDCCallback(clientID string, 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) + var uri string + if clientID != "" { + uri = fmt.Sprintf("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=%s&client_id=%s", resource, clientID) + } else { + uri = fmt.Sprintf("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=%s", resource) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, newAuthError("error creating http request to Azure Identity Provider", err) + } + req.Header.Add("Metadata", "true") + req.Header.Add("Accept", "application/json") + resp, err := httpClient.Do(req) + if err != nil { + return nil, newAuthError("error getting access token from Azure Identity Provider", err) + } + defer resp.Body.Close() + var azureResp struct { + AccessToken string `json:"access_token"` + ExpiresOn int64 `json:"expires_on,string"` + } + + if resp.StatusCode != http.StatusOK { + return nil, newAuthError(fmt.Sprintf("failed to get a valid response from Azure Identity Provider, http code: %d", resp.StatusCode), nil) + } + err = json.NewDecoder(resp.Body).Decode(&azureResp) + if err != nil { + return nil, newAuthError("failed parsing result from Azure Identity Provider", err) + } + expireTime := time.Unix(azureResp.ExpiresOn, 0) + return &OIDCCredential{ + AccessToken: azureResp.AccessToken, + ExpiresAt: &expireTime, + }, nil + } +} + func (oa *OIDCAuthenticator) getAccessToken( ctx context.Context, conn driver.Connection,