diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 180d039969..4c7340a8ea 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -30,6 +30,7 @@ import ( "go.mongodb.org/mongo-driver/mongo/writeconcern" "go.mongodb.org/mongo-driver/tag" "go.mongodb.org/mongo-driver/x/mongo/driver" + "go.mongodb.org/mongo-driver/x/mongo/driver/auth" "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" "go.mongodb.org/mongo-driver/x/mongo/driver/wiremessage" ) @@ -360,6 +361,34 @@ func (c *ClientOptions) validate() error { return fmt.Errorf("invalid server monitoring mode: %q", *mode) } + // OIDC Validation + if c.Auth != nil && c.Auth.AuthMechanism == auth.MongoDBOIDC { + if c.Auth.Password != "" { + return fmt.Errorf("password must not be set for the %s auth mechanism", auth.MongoDBOIDC) + } + if c.Auth.OIDCMachineCallback != nil && c.Auth.OIDCHumanCallback != nil { + return fmt.Errorf("cannot set both OIDCMachineCallback and OIDCHumanCallback, only one may be specified") + } + if env, ok := c.Auth.AuthMechanismProperties[auth.EnvironmentProp]; ok { + switch env { + case auth.GCPEnvironmentValue, auth.AzureEnvironmentValue: + if c.Auth.OIDCMachineCallback != nil { + return fmt.Errorf("OIDCMachineCallback cannot be specified with the %s %q", env, auth.EnvironmentProp) + } + if c.Auth.OIDCHumanCallback != nil { + return fmt.Errorf("OIDCHumanCallback cannot be specified with the %s %q", env, auth.EnvironmentProp) + } + if c.Auth.AuthMechanismProperties[auth.ResourceProp] == "" { + return fmt.Errorf("%q must be set for the %s %q", auth.ResourceProp, env, auth.EnvironmentProp) + } + default: + if c.Auth.AuthMechanismProperties[auth.ResourceProp] != "" { + return fmt.Errorf("%q must not be set for the %s %q", auth.ResourceProp, env, auth.EnvironmentProp) + } + } + } + } + return nil } diff --git a/mongo/options/clientoptions_test.go b/mongo/options/clientoptions_test.go index d6d39f298d..d60d4498f2 100644 --- a/mongo/options/clientoptions_test.go +++ b/mongo/options/clientoptions_test.go @@ -568,6 +568,36 @@ func TestClientOptions(t *testing.T) { Username: `C=US,ST=New York,L=New York City,O=MongoDB,OU=Drivers,CN=localhost`, }).SetTLSConfig(&tls.Config{Certificates: make([]tls.Certificate, 1)}), }, + { + "ALLOWED_HOSTS cannot be specified in URI connection", + "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:example.com", + &ClientOptions{ + err: fmt.Errorf( + `error validating uri: ALLOWED_HOSTS cannot be specified in the URI connection string for the "MONGODB-OIDC" auth mechanism, it must be specified through the ClientOptions directly`, + ), + HTTPClient: httputil.DefaultHTTPClient, + }, + }, + { + "colon in TOKEN_RESOURCE works as expected", + "mongodb://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=TOKEN_RESOURCE:mongodb://test-cluster", + &ClientOptions{ + Hosts: []string{"example.com"}, + Auth: &Credential{AuthMechanism: "MONGODB-OIDC", AuthSource: "$external", AuthMechanismProperties: map[string]string{"TOKEN_RESOURCE": "mongodb://test-cluster"}}, + err: nil, + HTTPClient: httputil.DefaultHTTPClient, + }, + }, + { + "comma in key:value pair causes error", + "mongodb://example.com/?authMechanismProperties=TOKEN_RESOURCE:mongodb://host1%2Chost2", + &ClientOptions{ + err: fmt.Errorf( + `error parsing uri: invalid authMechanism property`, + ), + HTTPClient: httputil.DefaultHTTPClient, + }, + }, } for _, tc := range testCases { @@ -795,6 +825,91 @@ func TestClientOptions(t *testing.T) { }) } }) + t.Run("OIDC auth configuration validation", func(t *testing.T) { + t.Parallel() + + emptyCb := func(_ context.Context, _ *OIDCArgs) (*OIDCCredential, error) { + return nil, nil + } + + testCases := []struct { + name string + opts *ClientOptions + err error + }{ + { + name: "password must not be set", + opts: Client().SetAuth(Credential{AuthMechanism: "MONGODB-OIDC", Password: "password"}), + err: fmt.Errorf("password must not be set for the MONGODB-OIDC auth mechanism"), + }, + { + name: "cannot set both OIDCMachineCallback and OIDCHumanCallback simultaneously", + opts: Client().SetAuth(Credential{AuthMechanism: "MONGODB-OIDC", + OIDCMachineCallback: emptyCb, OIDCHumanCallback: emptyCb}), + err: fmt.Errorf("cannot set both OIDCMachineCallback and OIDCHumanCallback, only one may be specified"), + }, + { + name: "cannot set OIDCMachineCallback in GCP Environment", + opts: Client().SetAuth(Credential{ + AuthMechanism: "MONGODB-OIDC", + OIDCMachineCallback: emptyCb, + AuthMechanismProperties: map[string]string{"ENVIRONMENT": "gcp"}, + }), + err: fmt.Errorf(`OIDCMachineCallback cannot be specified with the gcp "ENVIRONMENT"`), + }, + { + name: "cannot set OIDCMachineCallback in AZURE Environment", + opts: Client().SetAuth(Credential{ + AuthMechanism: "MONGODB-OIDC", + OIDCMachineCallback: emptyCb, + AuthMechanismProperties: map[string]string{"ENVIRONMENT": "azure"}, + }), + err: fmt.Errorf(`OIDCMachineCallback cannot be specified with the azure "ENVIRONMENT"`), + }, + { + name: "TOKEN_RESOURCE must be set in GCP Environment", + opts: Client().SetAuth(Credential{ + AuthMechanism: "MONGODB-OIDC", + AuthMechanismProperties: map[string]string{"ENVIRONMENT": "gcp"}, + }), + err: fmt.Errorf(`"TOKEN_RESOURCE" must be set for the gcp "ENVIRONMENT"`), + }, + { + name: "TOKEN_RESOURCE must be set in AZURE Environment", + opts: Client().SetAuth(Credential{ + AuthMechanism: "MONGODB-OIDC", + AuthMechanismProperties: map[string]string{"ENVIRONMENT": "azure"}, + }), + err: fmt.Errorf(`"TOKEN_RESOURCE" must be set for the azure "ENVIRONMENT"`), + }, + { + name: "TOKEN_RESOURCE must not be set in TEST Environment", + opts: Client().SetAuth(Credential{ + AuthMechanism: "MONGODB-OIDC", + AuthMechanismProperties: map[string]string{"ENVIRONMENT": "test", "TOKEN_RESOURCE": "stuff"}, + }), + err: fmt.Errorf(`"TOKEN_RESOURCE" must not be set for the test "ENVIRONMENT"`), + }, + { + name: "TOKEN_RESOURCE must not be set in any other Environment", + opts: Client().SetAuth(Credential{ + AuthMechanism: "MONGODB-OIDC", + AuthMechanismProperties: map[string]string{"ENVIRONMENT": "random env!", "TOKEN_RESOURCE": "stuff"}, + }), + err: fmt.Errorf(`"TOKEN_RESOURCE" must not be set for the random env! "ENVIRONMENT"`), + }, + } + for _, tc := range testCases { + tc := tc // Capture range variable. + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := tc.opts.Validate() + assert.Equal(t, tc.err, err, "want error %v, got error %v", tc.err, err) + }) + } + }) } func createCertPool(t *testing.T, paths ...string) *x509.CertPool { diff --git a/x/mongo/driver/auth/oidc.go b/x/mongo/driver/auth/oidc.go index 454a1f635d..0b71533b73 100644 --- a/x/mongo/driver/auth/oidc.go +++ b/x/mongo/driver/auth/oidc.go @@ -26,14 +26,23 @@ import ( // MongoDBOIDC is the string constant for the MONGODB-OIDC authentication mechanism. const MongoDBOIDC = "MONGODB-OIDC" -// const tokenResourceProp = "TOKEN_RESOURCE" -const environmentProp = "ENVIRONMENT" -const resourceProp = "TOKEN_RESOURCE" -const allowedHostsProp = "ALLOWED_HOSTS" +// EnvironmentProp is the property key name that specifies the environment for the OIDC authenticator. +const EnvironmentProp = "ENVIRONMENT" -const azureEnvironmentValue = "azure" -const gcpEnvironmentValue = "gcp" -const testEnvironmentValue = "test" +// ResourceProp is the property key name that specifies the token resource for GCP and AZURE OIDC auth. +const ResourceProp = "TOKEN_RESOURCE" + +// AllowedHostsProp is the property key name that specifies the allowed hosts for the OIDC authenticator. +const AllowedHostsProp = "ALLOWED_HOSTS" + +// AzureEnvironmentValue is the value for the Azure environment. +const AzureEnvironmentValue = "azure" + +// GCPEnvironmentValue is the value for the GCP environment. +const GCPEnvironmentValue = "gcp" + +// TestEnvironmentValue is the value for the test environment. +const TestEnvironmentValue = "test" const apiVersion = 1 const invalidateSleepTimeout = 100 * time.Millisecond @@ -104,18 +113,18 @@ func newOIDCAuthenticator(cred *Cred, httpClient *http.Client) (Authenticator, e return nil, fmt.Errorf("password cannot be specified for %q", MongoDBOIDC) } if cred.Props != nil { - if env, ok := cred.Props[environmentProp]; ok { + if env, ok := cred.Props[EnvironmentProp]; ok { switch strings.ToLower(env) { - case azureEnvironmentValue: + case AzureEnvironmentValue: fallthrough - case gcpEnvironmentValue: - if _, ok := cred.Props[resourceProp]; !ok { - return nil, fmt.Errorf("%q must be specified for %q %q", resourceProp, env, environmentProp) + case GCPEnvironmentValue: + if _, ok := cred.Props[ResourceProp]; !ok { + return nil, fmt.Errorf("%q must be specified for %q %q", ResourceProp, env, EnvironmentProp) } fallthrough - case testEnvironmentValue: + case TestEnvironmentValue: if cred.OIDCMachineCallback != nil || cred.OIDCHumanCallback != nil { - return nil, fmt.Errorf("OIDC callbacks are not allowed for %q %q", env, environmentProp) + return nil, fmt.Errorf("OIDC callbacks are not allowed for %q %q", env, EnvironmentProp) } } } @@ -151,7 +160,8 @@ func (oa *OIDCAuthenticator) setAllowedHosts() error { oa.allowedHosts = &defaultAllowedHosts return nil } - allowedHosts, ok := oa.AuthMechanismProperties[allowedHostsProp] + + allowedHosts, ok := oa.AuthMechanismProperties[AllowedHostsProp] if !ok { oa.allowedHosts = &defaultAllowedHosts return nil @@ -168,18 +178,18 @@ func (oa *OIDCAuthenticator) setAllowedHosts() error { func (oa *OIDCAuthenticator) validateConnectionAddressWithAllowedHosts(conn driver.Connection) error { if oa.allowedHosts == nil { // should be unreachable, but this is a safety check. - return newAuthError(fmt.Sprintf("%q missing", allowedHostsProp), nil) + return newAuthError(fmt.Sprintf("%q missing", AllowedHostsProp), nil) } allowedHosts := *oa.allowedHosts if len(allowedHosts) == 0 { - return newAuthError(fmt.Sprintf("empty %q specified", allowedHostsProp), nil) + return newAuthError(fmt.Sprintf("empty %q specified", AllowedHostsProp), nil) } for _, pattern := range allowedHosts { if pattern.MatchString(string(conn.Address())) { return nil } } - return newAuthError(fmt.Sprintf("address %q not allowed by %q: %v", conn.Address(), allowedHostsProp, allowedHosts), nil) + return newAuthError(fmt.Sprintf("address %q not allowed by %q: %v", conn.Address(), AllowedHostsProp, allowedHosts), nil) } type oidcOneStep struct { @@ -249,27 +259,27 @@ func (*oidcTwoStep) Completed() bool { } func (oa *OIDCAuthenticator) providerCallback() (OIDCCallback, error) { - env, ok := oa.AuthMechanismProperties[environmentProp] + env, ok := oa.AuthMechanismProperties[EnvironmentProp] if !ok { return nil, nil } switch env { - case azureEnvironmentValue: - resource, ok := oa.AuthMechanismProperties[resourceProp] + case AzureEnvironmentValue: + resource, ok := oa.AuthMechanismProperties[ResourceProp] if !ok { - return nil, newAuthError(fmt.Sprintf("%q must be specified for Azure OIDC", resourceProp), nil) + return nil, newAuthError(fmt.Sprintf("%q must be specified for Azure OIDC", ResourceProp), nil) } return getAzureOIDCCallback(oa.userName, resource, oa.httpClient), nil - case gcpEnvironmentValue: - resource, ok := oa.AuthMechanismProperties[resourceProp] + case GCPEnvironmentValue: + resource, ok := oa.AuthMechanismProperties[ResourceProp] if !ok { - return nil, newAuthError(fmt.Sprintf("%q must be specified for GCP OIDC", resourceProp), nil) + 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) + return nil, fmt.Errorf("%q %q not supported for MONGODB-OIDC", EnvironmentProp, env) } // getAzureOIDCCallback returns the callback for the Azure Identity Provider. diff --git a/x/mongo/driver/connstring/connstring.go b/x/mongo/driver/connstring/connstring.go index a8adafb8f8..4a7a01f4fb 100644 --- a/x/mongo/driver/connstring/connstring.go +++ b/x/mongo/driver/connstring/connstring.go @@ -24,6 +24,7 @@ import ( "go.mongodb.org/mongo-driver/internal/randutil" "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/x/mongo/driver/auth" "go.mongodb.org/mongo-driver/x/mongo/driver/dns" "go.mongodb.org/mongo-driver/x/mongo/driver/wiremessage" ) @@ -258,6 +259,16 @@ func (u *ConnString) Validate() error { } } + // Check for OIDC auth mechanism properties that cannot be set in the ConnString. + if u.AuthMechanism == auth.MongoDBOIDC { + if _, ok := u.AuthMechanismProperties[auth.AllowedHostsProp]; ok { + return fmt.Errorf( + "ALLOWED_HOSTS cannot be specified in the URI connection string for the %q auth mechanism, it must be specified through the ClientOptions directly", + auth.MongoDBOIDC, + ) + } + } + return nil }