diff --git a/api/v1/mdb/mongodb_types.go b/api/v1/mdb/mongodb_types.go index cfb03e166..aef83f5c6 100644 --- a/api/v1/mdb/mongodb_types.go +++ b/api/v1/mdb/mongodb_types.go @@ -1097,9 +1097,8 @@ type OIDCProviderConfig struct { // The identifier of the claim that includes the principal's IdP user group membership information. // Accept the default value unless your IdP uses a different claim, or you need a custom claim. // Required when selected GroupMembership as the authorization type, ignored otherwise - // +kubebuilder:default=groups // +kubebuilder:validation:Optional - GroupsClaim string `json:"groupsClaim,omitempty"` + GroupsClaim *string `json:"groupsClaim"` // Configure single-sign-on for human user access to Ops Manager deployments with Workforce Identity Federation. // For programmatic, application access to Ops Manager deployments use Workload Identity Federation. @@ -1111,7 +1110,7 @@ type OIDCProviderConfig struct { // registered with an external Identity Provider. // Required when selected Workforce Identity Federation authorization method // +kubebuilder:validation:Optional - ClientId string `json:"clientId,omitempty"` + ClientId *string `json:"clientId"` // Tokens that give users permission to request data from the authorization endpoint. // Only used for Workforce Identity Federation authorization method diff --git a/api/v1/mdb/mongodb_validation.go b/api/v1/mdb/mongodb_validation.go index 1468bef98..c8f6ea89e 100644 --- a/api/v1/mdb/mongodb_validation.go +++ b/api/v1/mdb/mongodb_validation.go @@ -256,11 +256,11 @@ func oidcProviderConfigIssuerURIValidator(config OIDCProviderConfig) func(DbComm func oidcProviderConfigClientIdValidator(config OIDCProviderConfig) func(DbCommonSpec) v1.ValidationResult { return func(_ DbCommonSpec) v1.ValidationResult { if config.AuthorizationMethod == OIDCAuthorizationMethodWorkforceIdentityFederation { - if config.ClientId == "" { + if config.ClientId == nil || *config.ClientId == "" { return v1.ValidationError("ClientId has to be specified in OIDC provider config %q with Workforce Identity Federation", config.ConfigurationName) } } else if config.AuthorizationMethod == OIDCAuthorizationMethodWorkloadIdentityFederation { - if config.ClientId != "" { + if config.ClientId != nil { return v1.ValidationWarning("ClientId will be ignored in OIDC provider config %q with Workload Identity Federation", config.ConfigurationName) } } @@ -284,11 +284,11 @@ func oidcProviderConfigRequestedScopesValidator(config OIDCProviderConfig) func( func oidcProviderConfigAuthorizationTypeValidator(config OIDCProviderConfig) func(DbCommonSpec) v1.ValidationResult { return func(_ DbCommonSpec) v1.ValidationResult { if config.AuthorizationType == OIDCAuthorizationTypeGroupMembership { - if config.GroupsClaim == "" { + if config.GroupsClaim == nil || *config.GroupsClaim == "" { return v1.ValidationError("GroupsClaim has to be specified in OIDC provider config %q when using Group Membership authorization", config.ConfigurationName) } } else if config.AuthorizationType == OIDCAuthorizationTypeUserID { - if config.GroupsClaim != "" { + if config.GroupsClaim != nil { return v1.ValidationWarning("GroupsClaim will be ignored in OIDC provider config %q when using User ID authorization", config.ConfigurationName) } } @@ -298,16 +298,14 @@ func oidcProviderConfigAuthorizationTypeValidator(config OIDCProviderConfig) fun } func oidcAuthRequiresEnterprise(d DbCommonSpec) v1.ValidationResult { - authSpec := d.Security.Authentication - if authSpec != nil && authSpec.IsOIDCEnabled() && !strings.HasSuffix(d.Version, "-ent") { + if d.Security.Authentication.IsOIDCEnabled() && !strings.HasSuffix(d.Version, "-ent") { return v1.ValidationError("Cannot enable OIDC authentication with MongoDB Community Builds") } return v1.ValidationSuccess() } func ldapAuthRequiresEnterprise(d DbCommonSpec) v1.ValidationResult { - authSpec := d.Security.Authentication - if authSpec != nil && authSpec.IsLDAPEnabled() && !strings.HasSuffix(d.Version, "-ent") { + if d.Security.Authentication.IsLDAPEnabled() && !strings.HasSuffix(d.Version, "-ent") { return v1.ValidationError("Cannot enable LDAP authentication with MongoDB Community Builds") } return v1.ValidationSuccess() diff --git a/api/v1/mdb/mongodb_validation_test.go b/api/v1/mdb/mongodb_validation_test.go index a2ba4e7b3..d7ac479ba 100644 --- a/api/v1/mdb/mongodb_validation_test.go +++ b/api/v1/mdb/mongodb_validation_test.go @@ -258,13 +258,13 @@ func TestOIDCAuthValidation(t *testing.T) { ConfigurationName: "provider", IssuerURI: "https://example1.com", AuthorizationMethod: OIDCAuthorizationMethodWorkforceIdentityFederation, - ClientId: "clientId1", + ClientId: ptr.To("clientId1"), }, { ConfigurationName: "provider", IssuerURI: "https://example2.com", AuthorizationMethod: OIDCAuthorizationMethodWorkforceIdentityFederation, - ClientId: "clientId2", + ClientId: ptr.To("clientId2"), }, }, }, @@ -281,13 +281,13 @@ func TestOIDCAuthValidation(t *testing.T) { ConfigurationName: "test-provider1", IssuerURI: "https://example1.com", AuthorizationMethod: OIDCAuthorizationMethodWorkforceIdentityFederation, - ClientId: "clientId1", + ClientId: ptr.To("clientId1"), }, { ConfigurationName: "test-provider2", IssuerURI: "https://example2.com", AuthorizationMethod: OIDCAuthorizationMethodWorkforceIdentityFederation, - ClientId: "clientId2", + ClientId: ptr.To("clientId2"), }, }, }, @@ -304,7 +304,7 @@ func TestOIDCAuthValidation(t *testing.T) { ConfigurationName: "test-provider-workforce1", IssuerURI: "https://example1.com", AuthorizationMethod: OIDCAuthorizationMethodWorkforceIdentityFederation, - ClientId: "clientId1", + ClientId: ptr.To("clientId1"), }, { ConfigurationName: "test-provider-workload2", @@ -376,7 +376,7 @@ func TestOIDCAuthValidation(t *testing.T) { ConfigurationName: "test-provider", IssuerURI: "https://example.com", AuthorizationMethod: OIDCAuthorizationMethodWorkloadIdentityFederation, - ClientId: "clientId", + ClientId: ptr.To("clientId"), }, }, }, @@ -410,7 +410,7 @@ func TestOIDCAuthValidation(t *testing.T) { ConfigurationName: "test-provider1", IssuerURI: "https://example.com", AuthorizationType: OIDCAuthorizationTypeGroupMembership, - GroupsClaim: "groups", + GroupsClaim: ptr.To("groups"), }, { ConfigurationName: "test-provider2", @@ -432,7 +432,7 @@ func TestOIDCAuthValidation(t *testing.T) { ConfigurationName: "test-provider1", IssuerURI: "https://example.com", AuthorizationType: OIDCAuthorizationTypeUserID, - GroupsClaim: "groups", + GroupsClaim: ptr.To("groups"), UserClaim: "sub", }, { @@ -456,13 +456,13 @@ func TestOIDCAuthValidation(t *testing.T) { ConfigurationName: "test-provider1", IssuerURI: "https://example.com", AuthorizationType: OIDCAuthorizationTypeGroupMembership, - GroupsClaim: "groups", + GroupsClaim: ptr.To("groups"), }, { ConfigurationName: "test-provider2", IssuerURI: "https://example.com", AuthorizationType: OIDCAuthorizationTypeGroupMembership, - GroupsClaim: "groups", + GroupsClaim: ptr.To("groups"), }, }, }, diff --git a/api/v1/mdb/zz_generated.deepcopy.go b/api/v1/mdb/zz_generated.deepcopy.go index e8c2e4813..ac09a290b 100644 --- a/api/v1/mdb/zz_generated.deepcopy.go +++ b/api/v1/mdb/zz_generated.deepcopy.go @@ -125,6 +125,13 @@ func (in *Authentication) DeepCopyInto(out *Authentication) { *out = new(Ldap) (*in).DeepCopyInto(*out) } + if in.OIDCProviderConfigs != nil { + in, out := &in.OIDCProviderConfigs, &out.OIDCProviderConfigs + *out = make([]OIDCProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.Agents.DeepCopyInto(&out.Agents) } @@ -948,6 +955,36 @@ func (in *MonitoringAgentConfig) DeepCopy() *MonitoringAgentConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCProviderConfig) DeepCopyInto(out *OIDCProviderConfig) { + *out = *in + if in.GroupsClaim != nil { + in, out := &in.GroupsClaim, &out.GroupsClaim + *out = new(string) + **out = **in + } + if in.ClientId != nil { + in, out := &in.ClientId, &out.ClientId + *out = new(string) + **out = **in + } + if in.RequestedScopes != nil { + in, out := &in.RequestedScopes, &out.RequestedScopes + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCProviderConfig. +func (in *OIDCProviderConfig) DeepCopy() *OIDCProviderConfig { + if in == nil { + return nil + } + out := new(OIDCProviderConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PersistenceConfigBuilder) DeepCopyInto(out *PersistenceConfigBuilder) { *out = *in diff --git a/config/crd/bases/mongodb.com_mongodb.yaml b/config/crd/bases/mongodb.com_mongodb.yaml index 880e89fa4..01101d360 100644 --- a/config/crd/bases/mongodb.com_mongodb.yaml +++ b/config/crd/bases/mongodb.com_mongodb.yaml @@ -1566,7 +1566,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. diff --git a/config/crd/bases/mongodb.com_mongodbmulticluster.yaml b/config/crd/bases/mongodb.com_mongodbmulticluster.yaml index a3dfcc0a1..1be90e11c 100644 --- a/config/crd/bases/mongodb.com_mongodbmulticluster.yaml +++ b/config/crd/bases/mongodb.com_mongodbmulticluster.yaml @@ -826,7 +826,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. diff --git a/config/crd/bases/mongodb.com_opsmanagers.yaml b/config/crd/bases/mongodb.com_opsmanagers.yaml index 2c33c0e49..42d1597d8 100644 --- a/config/crd/bases/mongodb.com_opsmanagers.yaml +++ b/config/crd/bases/mongodb.com_opsmanagers.yaml @@ -888,7 +888,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. diff --git a/controllers/om/automation_config.go b/controllers/om/automation_config.go index 9dffdffdb..bfb61c369 100644 --- a/controllers/om/automation_config.go +++ b/controllers/om/automation_config.go @@ -8,6 +8,7 @@ import ( "k8s.io/apimachinery/pkg/api/equality" "github.com/mongodb/mongodb-kubernetes/controllers/operator/ldap" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/oidc" "github.com/mongodb/mongodb-kubernetes/pkg/util" "github.com/mongodb/mongodb-kubernetes/pkg/util/generate" "github.com/mongodb/mongodb-kubernetes/pkg/util/maputil" @@ -20,10 +21,11 @@ import ( // configuration which are merged into the `Deployment` object before sending it back to Ops Manager. // As of right now only support configuring LogRotate for monitoring and backup via dedicated endpoints. type AutomationConfig struct { - Auth *Auth - AgentSSL *AgentSSL - Deployment Deployment - Ldap *ldap.Ldap + Auth *Auth + AgentSSL *AgentSSL + Deployment Deployment + Ldap *ldap.Ldap + OIDCProviderConfigs []oidc.ProviderConfig } // Apply merges the state of all concrete structs into the Deployment (map[string]interface{}) @@ -58,9 +60,66 @@ func applyInto(a AutomationConfig, into *Deployment) error { } (*into)["ldap"] = mergedLdap } + + if len(a.OIDCProviderConfigs) > 0 { + updateOIDCProviderConfigs(a, into) + } else { + // Clear oidcProviderConfigs if no configs are provided + delete(*into, "oidcProviderConfigs") + } + return nil } +func updateOIDCProviderConfigs(a AutomationConfig, into *Deployment) { + deploymentConfigs := make(map[string]map[string]any) + if configs, ok := a.Deployment["oidcProviderConfigs"]; ok { + configsSliceAny := cast.ToSlice(configs) + for _, configAny := range configsSliceAny { + config := configAny.(map[string]any) + configName := config["authNamePrefix"].(string) + deploymentConfigs[configName] = config + } + } + + result := make([]map[string]any, 0) + for _, config := range a.OIDCProviderConfigs { + deploymentConfig, ok := deploymentConfigs[config.AuthNamePrefix] + if !ok { + deploymentConfig = make(map[string]any) + } + + deploymentConfig["authNamePrefix"] = config.AuthNamePrefix + deploymentConfig["audience"] = config.Audience + deploymentConfig["issuerUri"] = config.IssuerUri + deploymentConfig["userClaim"] = config.UserClaim + deploymentConfig["supportsHumanFlows"] = config.SupportsHumanFlows + deploymentConfig["useAuthorizationClaim"] = config.UseAuthorizationClaim + + if config.ClientId == nil { + delete(deploymentConfig, "clientId") + } else { + deploymentConfig["clientId"] = config.ClientId + } + + if len(config.RequestedScopes) == 0 { + delete(deploymentConfig, "requestedScopes") + } else { + deploymentConfig["requestedScopes"] = config.RequestedScopes + } + + if config.GroupsClaim == nil { + delete(deploymentConfig, "groupsClaim") + } else { + deploymentConfig["groupsClaim"] = config.GroupsClaim + } + + result = append(result, deploymentConfig) + } + + (*into)["oidcProviderConfigs"] = result +} + // EqualsWithoutDeployment returns true if two AutomationConfig objects are meaningful equal by following the following conditions: // - Not taking AutomationConfig.Deployment into consideration. // - Serializing ac A and ac B to ensure that we remove util.MergoDelete before comparing those two. @@ -432,6 +491,19 @@ func BuildAutomationConfigFromDeployment(deployment Deployment) (*AutomationConf finalAutomationConfig.Ldap = acLdap } + oidcConfigsArray, ok := deployment["oidcProviderConfigs"] + if ok { + oidcMarshalled, err := json.Marshal(oidcConfigsArray) + if err != nil { + return nil, err + } + providerConfigs := make([]oidc.ProviderConfig, 0) + if err := json.Unmarshal(oidcMarshalled, &providerConfigs); err != nil { + return nil, err + } + finalAutomationConfig.OIDCProviderConfigs = providerConfigs + } + return finalAutomationConfig, nil } diff --git a/controllers/om/automation_config_test.go b/controllers/om/automation_config_test.go index 0b991ef0b..69300956e 100644 --- a/controllers/om/automation_config_test.go +++ b/controllers/om/automation_config_test.go @@ -7,8 +7,10 @@ import ( "github.com/spf13/cast" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/utils/ptr" "github.com/mongodb/mongodb-kubernetes/controllers/operator/ldap" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/oidc" "github.com/mongodb/mongodb-kubernetes/pkg/util" ) @@ -627,6 +629,54 @@ func TestAutomationConfigEquality(t *testing.T) { } ldapConfig2 := ldapConfig + oidcProviderConfigs := []oidc.ProviderConfig{ + { + AuthNamePrefix: "provider-1", + Audience: "aud", + IssuerUri: "https://provider1.okta.com", + UserClaim: "sub", + GroupsClaim: ptr.To("groups"), + SupportsHumanFlows: false, + UseAuthorizationClaim: true, + }, + { + AuthNamePrefix: "provider-2", + Audience: "aud", + IssuerUri: "https://provider2.okta.com", + ClientId: ptr.To("provider2-clientId"), + RequestedScopes: []string{"openid", "profile"}, + UserClaim: "sub", + SupportsHumanFlows: true, + UseAuthorizationClaim: false, + }, + } + + oidcProviderConfigsWithMergoDelete := []oidc.ProviderConfig{ + { + AuthNamePrefix: "provider-1", + Audience: "aud", + IssuerUri: "https://provider1.okta.com", + ClientId: nil, + UserClaim: "sub", + GroupsClaim: ptr.To("groups"), + SupportsHumanFlows: false, + UseAuthorizationClaim: true, + }, + { + AuthNamePrefix: "provider-2", + Audience: "aud", + IssuerUri: "https://provider2.okta.com", + ClientId: ptr.To("provider2-clientId"), + RequestedScopes: []string{"openid", "profile"}, + UserClaim: "sub", + GroupsClaim: nil, + SupportsHumanFlows: true, + UseAuthorizationClaim: false, + }, + } + + oidcProviderConfigs2 := oidcProviderConfigs + tests := map[string]struct { a *AutomationConfig b *AutomationConfig @@ -653,29 +703,33 @@ func TestAutomationConfigEquality(t *testing.T) { }, "Two the same configs created using the same structs are the same": { a: &AutomationConfig{ - Auth: &authConfig, - AgentSSL: &agentSSLConfig, - Deployment: deployment1, - Ldap: &ldapConfig, + Auth: &authConfig, + AgentSSL: &agentSSLConfig, + Deployment: deployment1, + Ldap: &ldapConfig, + OIDCProviderConfigs: oidcProviderConfigs, }, b: &AutomationConfig{ - Auth: &authConfig, - AgentSSL: &agentSSLConfig, - Deployment: deployment1, - Ldap: &ldapConfig, + Auth: &authConfig, + AgentSSL: &agentSSLConfig, + Deployment: deployment1, + Ldap: &ldapConfig, + OIDCProviderConfigs: oidcProviderConfigs, }, expectedEquality: true, }, "Two the same configs created using deep copy (and structs with different addresses) are the same": { a: &AutomationConfig{ - Auth: &authConfig, - AgentSSL: &agentSSLConfig, - Ldap: &ldapConfig, + Auth: &authConfig, + AgentSSL: &agentSSLConfig, + Ldap: &ldapConfig, + OIDCProviderConfigs: oidcProviderConfigs, }, b: &AutomationConfig{ - Auth: &authConfig2, - AgentSSL: &agentSSLConfig2, - Ldap: &ldapConfig2, + Auth: &authConfig2, + AgentSSL: &agentSSLConfig2, + Ldap: &ldapConfig2, + OIDCProviderConfigs: oidcProviderConfigs2, }, expectedEquality: true, }, @@ -685,7 +739,8 @@ func TestAutomationConfigEquality(t *testing.T) { NewAutoPwd: util.MergoDelete, LdapGroupDN: "abc", }, - Ldap: &ldapConfig, + Ldap: &ldapConfig, + OIDCProviderConfigs: oidcProviderConfigs, }, b: &AutomationConfig{ Auth: &Auth{ @@ -694,7 +749,8 @@ func TestAutomationConfigEquality(t *testing.T) { AgentSSL: &AgentSSL{ AutoPEMKeyFilePath: util.MergoDelete, }, - Ldap: &ldapConfig2, + Ldap: &ldapConfig2, + OIDCProviderConfigs: oidcProviderConfigsWithMergoDelete, }, expectedEquality: true, }, @@ -783,6 +839,204 @@ func TestLDAPIsMerged(t *testing.T) { assert.Contains(t, ldapMap, "CAFileContents") } +func TestOIDCProviderConfigsAreMerged(t *testing.T) { + tests := map[string]struct { + providerConfigs []oidc.ProviderConfig + expectedProviderConfigs any + }{ + "nil config list clears deployment oidcProviderConfigs": { + providerConfigs: nil, + expectedProviderConfigs: nil, + }, + "empty config list clears deployment oidcProviderConfigs": { + providerConfigs: make([]oidc.ProviderConfig, 0), + expectedProviderConfigs: nil, + }, + "single config not present in deployment oidcProviderConfigs": { + providerConfigs: []oidc.ProviderConfig{ + { + AuthNamePrefix: "provider-new", + Audience: "aud", + ClientId: nil, + IssuerUri: "https://provider-new.okta.com", + UserClaim: "sub", + GroupsClaim: ptr.To("groups"), + SupportsHumanFlows: false, + UseAuthorizationClaim: true, + }, + }, + expectedProviderConfigs: []map[string]any{ + { + "authNamePrefix": "provider-new", + "audience": "aud", + "issuerUri": "https://provider-new.okta.com", + "userClaim": "sub", + "groupsClaim": ptr.To("groups"), + "supportsHumanFlows": false, + "useAuthorizationClaim": true, + }, + }, + }, + "single config present in deployment oidcProviderConfig, without changes": { + providerConfigs: []oidc.ProviderConfig{ + { + AuthNamePrefix: "OIDC_WORKFORCE_USERID", + Audience: "aud", + IssuerUri: "https://provider.okta.com", + ClientId: ptr.To("oktaClientId"), + UserClaim: "sub", + GroupsClaim: nil, + RequestedScopes: []string{"openid"}, + SupportsHumanFlows: true, + UseAuthorizationClaim: false, + }, + }, + expectedProviderConfigs: []map[string]any{ + { + "authNamePrefix": "OIDC_WORKFORCE_USERID", + "audience": "aud", + "issuerUri": "https://provider.okta.com", + "clientId": ptr.To("oktaClientId"), + "requestedScopes": []string{ + "openid", + }, + "userClaim": "sub", + "supportsHumanFlows": true, + "useAuthorizationClaim": false, + "JWKSPollSecs": float64(360), + "additionalField": []any{ + "example.com", + }, + }, + }, + }, + "single config present in deployment oidcProviderConfig, but modified": { + providerConfigs: []oidc.ProviderConfig{ + { + AuthNamePrefix: "OIDC_WORKLOAD_USERID", + Audience: "aud", + ClientId: nil, + IssuerUri: "https://provider1.okta.com", + UserClaim: "sub", + GroupsClaim: ptr.To("groups"), + SupportsHumanFlows: false, + UseAuthorizationClaim: true, + }, + }, + expectedProviderConfigs: []map[string]any{ + { + "authNamePrefix": "OIDC_WORKLOAD_USERID", + "audience": "aud", + "issuerUri": "https://provider1.okta.com", + "userClaim": "sub", + "groupsClaim": ptr.To("groups"), + "supportsHumanFlows": false, + "useAuthorizationClaim": true, + "JWKSPollSecs": float64(360), + "additionalField": []interface{}{ + "example.com", + }, + }, + }, + }, + "multiple configs in deployment oidcProviderConfig": { + providerConfigs: []oidc.ProviderConfig{ + { + AuthNamePrefix: "OIDC_WORKFORCE_USERID", + Audience: "aud", + ClientId: ptr.To("oktaClientId"), + IssuerUri: "https://provider.okta.com", + UserClaim: "sub", + GroupsClaim: nil, + RequestedScopes: []string{"openid"}, + SupportsHumanFlows: true, + UseAuthorizationClaim: false, + }, + { + AuthNamePrefix: "OIDC_WORKLOAD_USERID", + Audience: "aud", + ClientId: nil, + IssuerUri: "https://provider.okta.com", + UserClaim: "sub", + GroupsClaim: nil, + RequestedScopes: []string{"openid"}, + SupportsHumanFlows: false, + UseAuthorizationClaim: false, + }, + { + AuthNamePrefix: "OIDC_WORKLOAD_GROUP_MEMBERSHIP", + Audience: "aud", + ClientId: nil, + IssuerUri: "https://provider.okta.com", + UserClaim: "sub", + GroupsClaim: ptr.To("groups"), + SupportsHumanFlows: false, + UseAuthorizationClaim: true, + }, + }, + expectedProviderConfigs: []map[string]any{ + { + "authNamePrefix": "OIDC_WORKFORCE_USERID", + "audience": "aud", + "issuerUri": "https://provider.okta.com", + "clientId": ptr.To("oktaClientId"), + "requestedScopes": []string{ + "openid", + }, + "userClaim": "sub", + "supportsHumanFlows": true, + "useAuthorizationClaim": false, + "JWKSPollSecs": float64(360), + "additionalField": []interface{}{ + "example.com", + }, + }, + { + "authNamePrefix": "OIDC_WORKLOAD_USERID", + "audience": "aud", + "issuerUri": "https://provider.okta.com", + "requestedScopes": []string{ + "openid", + }, + "userClaim": "sub", + "supportsHumanFlows": false, + "useAuthorizationClaim": false, + "JWKSPollSecs": float64(360), + "additionalField": []interface{}{ + "example.com", + }, + }, + { + "authNamePrefix": "OIDC_WORKLOAD_GROUP_MEMBERSHIP", + "audience": "aud", + "issuerUri": "https://provider.okta.com", + "userClaim": "sub", + "groupsClaim": ptr.To("groups"), + "supportsHumanFlows": false, + "useAuthorizationClaim": true, + "JWKSPollSecs": float64(360), + "additionalField": []interface{}{ + "example.com", + }, + }, + }, + }, + } + + for testName, testParameters := range tests { + t.Run(testName, func(t *testing.T) { + ac := getTestAutomationConfig() + ac.OIDCProviderConfigs = testParameters.providerConfigs + if err := ac.Apply(); err != nil { + t.Fatal(err) + } + + providerConfigs := ac.Deployment["oidcProviderConfigs"] + assert.Equal(t, testParameters.expectedProviderConfigs, providerConfigs) + }) + } +} + func TestApplyInto(t *testing.T) { config := AutomationConfig{ Auth: NewAuth(), diff --git a/controllers/om/testdata/automation_config.json b/controllers/om/testdata/automation_config.json index e2e9041e8..2ae929656 100644 --- a/controllers/om/testdata/automation_config.json +++ b/controllers/om/testdata/automation_config.json @@ -320,5 +320,51 @@ "clientCertificateMode": "OPTIONAL" }, "version": null, - "sharding": [] + "sharding": [], + "oidcProviderConfigs": [ + { + "audience": "aud", + "issuerUri": "https://provider.okta.com", + "clientId": "oktaClientId", + "requestedScopes": [ + "openid" + ], + "userClaim": "sub", + "JWKSPollSecs": 360, + "authNamePrefix": "OIDC_WORKFORCE_USERID", + "supportsHumanFlows": true, + "useAuthorizationClaim": false, + "additionalField": [ + "example.com" + ] + }, + { + "audience": "aud", + "issuerUri": "https://provider.okta.com", + "requestedScopes": [ + "openid" + ], + "userClaim": "sub", + "JWKSPollSecs": 360, + "authNamePrefix": "OIDC_WORKLOAD_USERID", + "supportsHumanFlows": false, + "useAuthorizationClaim": false, + "additionalField": [ + "example.com" + ] + }, + { + "audience": "aud", + "issuerUri": "https://provider.okta.com", + "userClaim": "sub", + "groupsClaim": "groups", + "JWKSPollSecs": 360, + "authNamePrefix": "OIDC_WORKLOAD_GROUP_MEMBERSHIP", + "supportsHumanFlows": false, + "useAuthorizationClaim": true, + "additionalField": [ + "example.com" + ] + } + ] } diff --git a/controllers/operator/authentication/authentication.go b/controllers/operator/authentication/authentication.go index 1ac47f9d1..1744317cd 100644 --- a/controllers/operator/authentication/authentication.go +++ b/controllers/operator/authentication/authentication.go @@ -7,6 +7,7 @@ import ( mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" "github.com/mongodb/mongodb-kubernetes/controllers/om" "github.com/mongodb/mongodb-kubernetes/controllers/operator/ldap" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/oidc" "github.com/mongodb/mongodb-kubernetes/pkg/util" ) @@ -16,6 +17,7 @@ type AuthResource interface { GetNamespace() string GetSecurity() *mdbv1.Security IsLDAPEnabled() bool + IsOIDCEnabled() bool GetLDAP(password, caContents string) *ldap.Ldap } @@ -52,6 +54,8 @@ type Options struct { // Only required if LDAP is configured as an authentication mechanism Ldap *ldap.Ldap + OIDCProviderConfigs []oidc.ProviderConfig + AutoUser string AutoPwd string diff --git a/controllers/operator/authentication/authentication_mechanism.go b/controllers/operator/authentication/authentication_mechanism.go index 679ea1c7f..90e6877e8 100644 --- a/controllers/operator/authentication/authentication_mechanism.go +++ b/controllers/operator/authentication/authentication_mechanism.go @@ -34,6 +34,7 @@ const ( ScramSha1 MechanismName = "SCRAM-SHA-1" MongoDBX509 MechanismName = "MONGODB-X509" LDAPPlain MechanismName = "PLAIN" + MongoDBOIDC MechanismName = "MONGODB-OIDC" // MongoDBCR is an umbrella term for SCRAM-SHA-1 and MONGODB-CR for legacy reasons, once MONGODB-CR // is enabled, users can auth with SCRAM-SHA-1 credentials @@ -65,7 +66,7 @@ func (m MechanismList) Contains(mechanismName MechanismName) bool { // supportedMechanisms returns a list of all supported authentication mechanisms // that can be configured by the Operator -var supportedMechanisms = []MechanismName{ScramSha256, MongoDBCR, MongoDBX509, LDAPPlain} +var supportedMechanisms = []MechanismName{ScramSha256, MongoDBCR, MongoDBX509, LDAPPlain, MongoDBOIDC} // mechanismsToDisable returns mechanisms which need to be disabled // based on the currently supported authentication mechanisms and the desiredMechanisms @@ -102,6 +103,8 @@ func convertToMechanismOrPanic(mechanismModeInCR string, ac *om.AutomationConfig return getMechanismByName(MongoDBCR) case util.SCRAMSHA256: return getMechanismByName(ScramSha256) + case util.OIDC: + return getMechanismByName(MongoDBOIDC) case util.SCRAM: // if we have already configured authentication, and it has been set to MONGODB-CR/SCRAM-SHA-1 // we can not transition. This needs to be done in the UI @@ -130,6 +133,8 @@ func getMechanismByName(name MechanismName) Mechanism { return &connectionX509{} case LDAPPlain: return &ldapAuthMechanism{} + case MongoDBOIDC: + return &oidcAuthMechanism{} } panic(xerrors.Errorf("unknown mechanism name %s", name)) diff --git a/controllers/operator/authentication/configure_authentication_test.go b/controllers/operator/authentication/configure_authentication_test.go index ae21a6611..8e8ae95df 100644 --- a/controllers/operator/authentication/configure_authentication_test.go +++ b/controllers/operator/authentication/configure_authentication_test.go @@ -153,12 +153,12 @@ func TestGetCorrectAuthMechanismFromVersion(t *testing.T) { mechanismList := convertToMechanismList([]string{"X509"}, ac) assert.Len(t, mechanismList, 1) - assert.Contains(t, mechanismList, MongoDBX509Mechanism) + assert.Contains(t, mechanismList, mongoDBX509Mechanism) mechanismList = convertToMechanismList([]string{"SCRAM", "X509"}, ac) - assert.Contains(t, mechanismList, ScramSha256Mechanism) - assert.Contains(t, mechanismList, MongoDBX509Mechanism) + assert.Contains(t, mechanismList, scramSha256Mechanism) + assert.Contains(t, mechanismList, mongoDBX509Mechanism) // enable MONGODB-CR ac.Auth.AutoAuthMechanism = "MONGODB-CR" @@ -166,8 +166,8 @@ func TestGetCorrectAuthMechanismFromVersion(t *testing.T) { mechanismList = convertToMechanismList([]string{"SCRAM", "X509"}, ac) - assert.Contains(t, mechanismList, MongoDBCRMechanism) - assert.Contains(t, mechanismList, MongoDBX509Mechanism) + assert.Contains(t, mechanismList, mongoDBCRMechanism) + assert.Contains(t, mechanismList, mongoDBX509Mechanism) } func assertAuthenticationEnabled(t *testing.T, auth *om.Auth) { diff --git a/controllers/operator/authentication/ldap_test.go b/controllers/operator/authentication/ldap_test.go index 225e1f39c..0b619e3df 100644 --- a/controllers/operator/authentication/ldap_test.go +++ b/controllers/operator/authentication/ldap_test.go @@ -11,7 +11,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/controllers/operator/ldap" ) -var LDAPPlainMechanism = getMechanismByName(LDAPPlain) +var ldapPlainMechanism = getMechanismByName(LDAPPlain) func TestLdapDeploymentMechanism(t *testing.T) { conn := om.NewMockedOmConnection(om.NewDeployment()) @@ -24,7 +24,7 @@ func TestLdapDeploymentMechanism(t *testing.T) { }, } - err := LDAPPlainMechanism.EnableDeploymentAuthentication(conn, opts, zap.S()) + err := ldapPlainMechanism.EnableDeploymentAuthentication(conn, opts, zap.S()) require.NoError(t, err) ac, err := conn.ReadAutomationConfig() @@ -34,7 +34,7 @@ func TestLdapDeploymentMechanism(t *testing.T) { assert.Equal(t, "Servers", ac.Ldap.Servers) assert.Equal(t, "BindMethod", ac.Ldap.BindMethod) - err = LDAPPlainMechanism.DisableDeploymentAuthentication(conn, zap.S()) + err = ldapPlainMechanism.DisableDeploymentAuthentication(conn, zap.S()) require.NoError(t, err) ac, err = conn.ReadAutomationConfig() @@ -55,7 +55,7 @@ func TestLdapEnableAgentAuthentication(t *testing.T) { AutoPwd: "LDAPPassword.", } - err := LDAPPlainMechanism.EnableAgentAuthentication(conn, opts, zap.S()) + err := ldapPlainMechanism.EnableAgentAuthentication(conn, opts, zap.S()) require.NoError(t, err) ac, err := conn.ReadAutomationConfig() @@ -80,5 +80,5 @@ func TestLDAP_DisableAgentAuthentication(t *testing.T) { }, } - assertAgentAuthenticationDisabled(t, LDAPPlainMechanism, conn, opts) + assertAgentAuthenticationDisabled(t, ldapPlainMechanism, conn, opts) } diff --git a/controllers/operator/authentication/oidc.go b/controllers/operator/authentication/oidc.go new file mode 100644 index 000000000..7e9bf4c2c --- /dev/null +++ b/controllers/operator/authentication/oidc.go @@ -0,0 +1,135 @@ +package authentication + +import ( + "fmt" + "slices" + "strings" + + "go.uber.org/zap" + "golang.org/x/xerrors" + + mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb" + "github.com/mongodb/mongodb-kubernetes/controllers/om" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/oidc" + "github.com/mongodb/mongodb-kubernetes/pkg/util/stringutil" +) + +type oidcAuthMechanism struct{} + +func (o *oidcAuthMechanism) GetName() MechanismName { + return MongoDBOIDC +} + +func (o *oidcAuthMechanism) EnableAgentAuthentication(_ om.Connection, _ Options, _ *zap.SugaredLogger) error { + return xerrors.Errorf("OIDC agent authentication is not supported") +} + +func (o *oidcAuthMechanism) DisableAgentAuthentication(_ om.Connection, _ *zap.SugaredLogger) error { + return xerrors.Errorf("OIDC agent authentication is not supported") +} + +func (o *oidcAuthMechanism) EnableDeploymentAuthentication(conn om.Connection, opts Options, log *zap.SugaredLogger) error { + return conn.ReadUpdateAutomationConfig(func(ac *om.AutomationConfig) error { + if !stringutil.Contains(ac.Auth.DeploymentAuthMechanisms, string(MongoDBOIDC)) { + ac.Auth.DeploymentAuthMechanisms = append(ac.Auth.DeploymentAuthMechanisms, string(MongoDBOIDC)) + } + ac.OIDCProviderConfigs = opts.OIDCProviderConfigs + + return nil + }, log) +} + +func (o *oidcAuthMechanism) DisableDeploymentAuthentication(conn om.Connection, log *zap.SugaredLogger) error { + return conn.ReadUpdateAutomationConfig(func(ac *om.AutomationConfig) error { + ac.Auth.DeploymentAuthMechanisms = stringutil.Remove(ac.Auth.DeploymentAuthMechanisms, string(MongoDBOIDC)) + ac.OIDCProviderConfigs = nil + + return nil + }, log) +} + +func (o *oidcAuthMechanism) IsAgentAuthenticationConfigured(*om.AutomationConfig, Options) bool { + return false +} + +func (o *oidcAuthMechanism) IsDeploymentAuthenticationConfigured(ac *om.AutomationConfig, opts Options) bool { + return o.IsDeploymentAuthenticationEnabled(ac) && oidcProviderConfigsEqual(ac.OIDCProviderConfigs, opts.OIDCProviderConfigs) +} + +func (o *oidcAuthMechanism) IsDeploymentAuthenticationEnabled(ac *om.AutomationConfig) bool { + return stringutil.Contains(ac.Auth.DeploymentAuthMechanisms, string(MongoDBOIDC)) +} + +func oidcProviderConfigsEqual(lhs []oidc.ProviderConfig, rhs []oidc.ProviderConfig) bool { + if len(lhs) != len(rhs) { + return false + } + + lhsSorted := sortOIDCPProviderConfigs(lhs) + rhsSorted := sortOIDCPProviderConfigs(rhs) + + return slices.EqualFunc(lhsSorted, rhsSorted, oidcProviderConfigEqual) +} + +func sortOIDCPProviderConfigs(configs []oidc.ProviderConfig) []oidc.ProviderConfig { + configsSeq := slices.Values(configs) + return slices.SortedFunc(configsSeq, func(l, r oidc.ProviderConfig) int { + return strings.Compare(l.AuthNamePrefix, r.AuthNamePrefix) + }) +} + +func oidcProviderConfigEqual(l, r oidc.ProviderConfig) bool { + return l.AuthNamePrefix == r.AuthNamePrefix && + l.Audience == r.Audience && + l.IssuerUri == r.IssuerUri && + slices.Equal(l.RequestedScopes, r.RequestedScopes) && + l.UserClaim == r.UserClaim && + l.GroupsClaim == r.GroupsClaim && + l.SupportsHumanFlows == r.SupportsHumanFlows && + l.UseAuthorizationClaim == r.UseAuthorizationClaim +} + +func MapOIDCProviderConfigs(oidcProviderConfigs []mdbv1.OIDCProviderConfig) []oidc.ProviderConfig { + if len(oidcProviderConfigs) == 0 { + return nil + } + + result := make([]oidc.ProviderConfig, len(oidcProviderConfigs)) + for i, providerConfig := range oidcProviderConfigs { + result[i] = oidc.ProviderConfig{ + AuthNamePrefix: providerConfig.ConfigurationName, + Audience: providerConfig.Audience, + IssuerUri: providerConfig.IssuerURI, + ClientId: providerConfig.ClientId, + RequestedScopes: providerConfig.RequestedScopes, + UserClaim: providerConfig.UserClaim, + GroupsClaim: providerConfig.GroupsClaim, + SupportsHumanFlows: mapToSupportHumanFlows(providerConfig.AuthorizationMethod), + UseAuthorizationClaim: mapToUseAuthorizationClaim(providerConfig.AuthorizationType), + } + } + + return result +} + +func mapToSupportHumanFlows(authMethod mdbv1.OIDCAuthorizationMethod) bool { + switch authMethod { + case mdbv1.OIDCAuthorizationMethodWorkforceIdentityFederation: + return true + case mdbv1.OIDCAuthorizationMethodWorkloadIdentityFederation: + return false + } + + panic(fmt.Sprintf("unsupported OIDC authorization method: %s", authMethod)) +} + +func mapToUseAuthorizationClaim(authType mdbv1.OIDCAuthorizationType) bool { + switch authType { + case mdbv1.OIDCAuthorizationTypeGroupMembership: + return true + case mdbv1.OIDCAuthorizationTypeUserID: + return false + } + + panic(fmt.Sprintf("unsupported OIDC authorization type: %s", authType)) +} diff --git a/controllers/operator/authentication/oidc_test.go b/controllers/operator/authentication/oidc_test.go new file mode 100644 index 000000000..6460db803 --- /dev/null +++ b/controllers/operator/authentication/oidc_test.go @@ -0,0 +1,96 @@ +package authentication + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "k8s.io/utils/ptr" + + "github.com/mongodb/mongodb-kubernetes/controllers/om" + "github.com/mongodb/mongodb-kubernetes/controllers/operator/oidc" +) + +var mongoDBOIDCMechanism = getMechanismByName(MongoDBOIDC) + +func TestOIDC_EnableDeploymentAuthentication(t *testing.T) { + conn := om.NewMockedOmConnection(om.NewDeployment()) + ac, err := conn.ReadAutomationConfig() + require.NoError(t, err) + assert.Empty(t, ac.OIDCProviderConfigs) + assert.Empty(t, ac.Auth.DeploymentAuthMechanisms) + + providerConfigs := []oidc.ProviderConfig{ + { + AuthNamePrefix: "okta", + Audience: "aud", + IssuerUri: "https://okta.mongodb.com", + ClientId: ptr.To("client1"), + RequestedScopes: []string{"openid", "profile"}, + UserClaim: "sub", + SupportsHumanFlows: true, + UseAuthorizationClaim: false, + }, + { + AuthNamePrefix: "congito", + Audience: "aud", + IssuerUri: "https://congito.mongodb.com", + ClientId: ptr.To("client2"), + UserClaim: "sub", + GroupsClaim: ptr.To("groups"), + SupportsHumanFlows: false, + UseAuthorizationClaim: true, + }, + } + + opts := Options{ + Mechanisms: []string{string(MongoDBOIDC)}, + OIDCProviderConfigs: providerConfigs, + } + + configured := mongoDBOIDCMechanism.IsDeploymentAuthenticationConfigured(ac, opts) + assert.False(t, configured) + + err = mongoDBOIDCMechanism.EnableDeploymentAuthentication(conn, opts, zap.S()) + require.NoError(t, err) + + ac, err = conn.ReadAutomationConfig() + require.NoError(t, err) + assert.Contains(t, ac.Auth.DeploymentAuthMechanisms, string(MongoDBOIDC)) + assert.Equal(t, providerConfigs, ac.OIDCProviderConfigs) + + configured = mongoDBOIDCMechanism.IsDeploymentAuthenticationConfigured(ac, opts) + assert.True(t, configured) + + err = mongoDBOIDCMechanism.DisableDeploymentAuthentication(conn, zap.S()) + require.NoError(t, err) + + ac, err = conn.ReadAutomationConfig() + require.NoError(t, err) + + configured = mongoDBOIDCMechanism.IsDeploymentAuthenticationConfigured(ac, opts) + assert.False(t, configured) + + assert.NotContains(t, ac.Auth.DeploymentAuthMechanisms, string(MongoDBOIDC)) + assert.Empty(t, ac.OIDCProviderConfigs) +} + +func TestOIDC_EnableAgentAuthentication(t *testing.T) { + conn := om.NewMockedOmConnection(om.NewDeployment()) + opts := Options{ + Mechanisms: []string{string(MongoDBOIDC)}, + } + + ac, err := conn.ReadAutomationConfig() + require.NoError(t, err) + + configured := mongoDBOIDCMechanism.IsAgentAuthenticationConfigured(ac, opts) + assert.False(t, configured) + + err = mongoDBOIDCMechanism.EnableAgentAuthentication(conn, opts, zap.S()) + require.Error(t, err) + + err = mongoDBOIDCMechanism.DisableAgentAuthentication(conn, zap.S()) + require.Error(t, err) +} diff --git a/controllers/operator/authentication/scramsha_test.go b/controllers/operator/authentication/scramsha_test.go index b4cc63cd1..1c97e9943 100644 --- a/controllers/operator/authentication/scramsha_test.go +++ b/controllers/operator/authentication/scramsha_test.go @@ -12,9 +12,9 @@ import ( ) var ( - MongoDBCRMechanism = getMechanismByName(MongoDBCR) - ScramSha1Mechanism = getMechanismByName(ScramSha1) - ScramSha256Mechanism = getMechanismByName(ScramSha256) + mongoDBCRMechanism = getMechanismByName(MongoDBCR) + scramSha1Mechanism = getMechanismByName(ScramSha1) + scramSha256Mechanism = getMechanismByName(ScramSha256) ) func TestAgentsAuthentication(t *testing.T) { @@ -23,13 +23,13 @@ func TestAgentsAuthentication(t *testing.T) { } tests := map[string]TestConfig{ "SCRAM-SHA-1": { - mechanism: ScramSha1Mechanism, + mechanism: scramSha1Mechanism, }, "SCRAM-SHA-256": { - mechanism: ScramSha256Mechanism, + mechanism: scramSha256Mechanism, }, "CR": { - mechanism: MongoDBCRMechanism, + mechanism: mongoDBCRMechanism, }, } for testName, testConfig := range tests { @@ -65,10 +65,10 @@ func TestAgentsAuthentication(t *testing.T) { func TestScramSha1_DisableAgentAuthentication(t *testing.T) { conn := om.NewMockedOmConnection(om.NewDeployment()) - assertAgentAuthenticationDisabled(t, ScramSha1Mechanism, conn, Options{}) + assertAgentAuthenticationDisabled(t, scramSha1Mechanism, conn, Options{}) } func TestScramSha256_DisableAgentAuthentication(t *testing.T) { conn := om.NewMockedOmConnection(om.NewDeployment()) - assertAgentAuthenticationDisabled(t, ScramSha256Mechanism, conn, Options{}) + assertAgentAuthenticationDisabled(t, scramSha256Mechanism, conn, Options{}) } diff --git a/controllers/operator/authentication/x509_test.go b/controllers/operator/authentication/x509_test.go index 05016aee3..f342b6702 100644 --- a/controllers/operator/authentication/x509_test.go +++ b/controllers/operator/authentication/x509_test.go @@ -12,7 +12,7 @@ import ( "github.com/mongodb/mongodb-kubernetes/pkg/util" ) -var MongoDBX509Mechanism = getMechanismByName(MongoDBX509) +var mongoDBX509Mechanism = getMechanismByName(MongoDBX509) func TestX509EnableAgentAuthentication(t *testing.T) { conn := om.NewMockedOmConnection(om.NewDeployment()) @@ -25,7 +25,7 @@ func TestX509EnableAgentAuthentication(t *testing.T) { }, AuthoritativeSet: true, } - if err := MongoDBX509Mechanism.EnableAgentAuthentication(conn, options, zap.S()); err != nil { + if err := mongoDBX509Mechanism.EnableAgentAuthentication(conn, options, zap.S()); err != nil { t.Fatal(err) } @@ -55,14 +55,14 @@ func TestX509_DisableAgentAuthentication(t *testing.T) { AutomationSubject: validSubject("automation"), }, } - assertAgentAuthenticationDisabled(t, MongoDBX509Mechanism, conn, opts) + assertAgentAuthenticationDisabled(t, mongoDBX509Mechanism, conn, opts) } func TestX509_DeploymentConfigured(t *testing.T) { conn := om.NewMockedOmConnection(om.NewDeployment()) opts := Options{AgentMechanism: "SCRAM", CAFilePath: util.CAFilePathInContainer} - assertDeploymentMechanismsConfigured(t, MongoDBX509Mechanism, conn, opts) + assertDeploymentMechanismsConfigured(t, mongoDBX509Mechanism, conn, opts) ac, err := conn.ReadAutomationConfig() require.NoError(t, err) diff --git a/controllers/operator/common_controller.go b/controllers/operator/common_controller.go index 436257e64..b4a289510 100644 --- a/controllers/operator/common_controller.go +++ b/controllers/operator/common_controller.go @@ -409,6 +409,10 @@ func (r *ReconcileCommonController) updateOmAuthentication(ctx context.Context, authOpts.Ldap = ar.GetLDAP(bindUserPassword, caContents) } + if ar.IsOIDCEnabled() { + authOpts.OIDCProviderConfigs = authentication.MapOIDCProviderConfigs(ar.GetSecurity().Authentication.OIDCProviderConfigs) + } + log.Debugf("Using authentication options %+v", authentication.Redact(authOpts)) agentSecretSelector := ar.GetSecurity().AgentClientCertificateSecretName(ar.GetName()) diff --git a/controllers/operator/mongodbuser_controller.go b/controllers/operator/mongodbuser_controller.go index 54e9de068..ac2f5cb2f 100644 --- a/controllers/operator/mongodbuser_controller.go +++ b/controllers/operator/mongodbuser_controller.go @@ -481,7 +481,11 @@ func waitForReadyState(conn om.Connection, log *zap.SugaredLogger) error { } func externalAuthMechanismsAvailable(mechanisms []string) bool { - return stringutil.ContainsAny(mechanisms, util.AutomationConfigLDAPOption, util.AutomationConfigX509Option) + return stringutil.ContainsAny(mechanisms, + util.AutomationConfigLDAPOption, + util.AutomationConfigX509Option, + util.AutomationConfigOIDCOption, + ) } func getAnnotationsForUserResource(user *userv1.MongoDBUser) (map[string]string, error) { diff --git a/controllers/operator/oidc/oidc_types.go b/controllers/operator/oidc/oidc_types.go new file mode 100644 index 000000000..88cb871ec --- /dev/null +++ b/controllers/operator/oidc/oidc_types.go @@ -0,0 +1,13 @@ +package oidc + +type ProviderConfig struct { + AuthNamePrefix string `json:"authNamePrefix"` + Audience string `json:"audience"` + IssuerUri string `json:"issuerUri"` + ClientId *string `json:"clientId"` + RequestedScopes []string `json:"requestedScopes"` + UserClaim string `json:"userClaim"` + GroupsClaim *string `json:"groupsClaim"` + SupportsHumanFlows bool `json:"supportsHumanFlows"` + UseAuthorizationClaim bool `json:"useAuthorizationClaim"` +} diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/e2e_mongodb_validation_webhook.py b/docker/mongodb-kubernetes-tests/tests/webhooks/e2e_mongodb_validation_webhook.py index 716ebe854..0b60aa304 100644 --- a/docker/mongodb-kubernetes-tests/tests/webhooks/e2e_mongodb_validation_webhook.py +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/e2e_mongodb_validation_webhook.py @@ -79,6 +79,71 @@ def test_replicaset_members_is_specified(self): exception_reason="'spec.members' must be specified if type of MongoDB is ReplicaSet", ) + def test_oidc_auth_with_mongodb_non_enterprise(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_mongodb_non_enterprise.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason="Cannot enable OIDC authentication with MongoDB Community Builds", + ) + + def test_oidc_auth_with_single_method(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_single_auth.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason="OIDC authentication cannot be used as the only authentication mechanism", + ) + + def test_oidc_auth_with_duplicate_config_name(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_duplicate_config_name.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason="OIDC provider config name OIDC-test is not unique", + ) + + def test_oidc_auth_with_multiple_workforce(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_multiple_workforce.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason="Only one OIDC provider config can be configured with Workforce Identity Federation. " + + "The following configs are configured with Workforce Identity Federation: OIDC-test, OIDC-test-2", + ) + + def test_oidc_auth_with_invalid_uri(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_invalid_uri.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason='Invalid IssuerURI in OIDC provider config \\"OIDC-test\\": invalid URL scheme (http or https): ', + ) + + def test_oidc_auth_with_invalid_clientid(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_invalid_clientid.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason='ClientId has to be specified in OIDC provider config \\"OIDC-test\\" with Workforce Identity Federation', + ) + + def test_oidc_auth_with_missing_groupclaim(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_missing_groupclaim.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason='GroupsClaim has to be specified in OIDC provider config \\"OIDC-test\\" when using Group Membership authorization', + ) + + def test_oidc_auth_with_no_providers(self): + resource = yaml.safe_load(open(yaml_fixture("invalid_oidc_no_providers.yaml"))) + self.create_custom_resource_from_object( + self.get_namespace(), + resource, + exception_reason="At least one OIDC provider config needs to be specified when OIDC authentication is enabled", + ) + def test_replicaset_members_is_specified_without_webhook(self): self._assert_validates_without_webhook( "mdbpolicy.mongodb.com", diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_duplicate_config_name.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_duplicate_config_name.yaml new file mode 100644 index 000000000..1e4710c23 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_duplicate_config_name.yaml @@ -0,0 +1,41 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + clientId: "example-client-id" + issuerURI: "https://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" + - audience: "example-audience-2" + clientId: "example-client-id-2" + issuerURI: "https://example-issuer-2.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_invalid_clientid.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_invalid_clientid.yaml new file mode 100644 index 000000000..70a9d0599 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_invalid_clientid.yaml @@ -0,0 +1,31 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + issuerURI: "https://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkforceIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_invalid_uri.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_invalid_uri.yaml new file mode 100644 index 000000000..e5509790e --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_invalid_uri.yaml @@ -0,0 +1,32 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + clientId: "example-client-id" + issuerURI: "hps://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_missing_groupclaim.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_missing_groupclaim.yaml new file mode 100644 index 000000000..3b883b0bc --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_missing_groupclaim.yaml @@ -0,0 +1,31 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + clientId: "example-client-id" + issuerURI: "https://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_mongodb_non_enterprise.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_mongodb_non_enterprise.yaml new file mode 100644 index 000000000..340c810da --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_mongodb_non_enterprise.yaml @@ -0,0 +1,32 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5 + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + clientId: "example-client-id" + issuerURI: "https://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_multiple_workforce.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_multiple_workforce.yaml new file mode 100644 index 000000000..b0a81bd69 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_multiple_workforce.yaml @@ -0,0 +1,41 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + clientId: "example-client-id" + issuerURI: "https://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkforceIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" + - audience: "example-audience-2" + clientId: "example-client-id-2" + issuerURI: "https://example-issuer-2.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkforceIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test-2" diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_no_providers.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_no_providers.yaml new file mode 100644 index 000000000..5824c170b --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_no_providers.yaml @@ -0,0 +1,22 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_single_auth.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_single_auth.yaml new file mode 100644 index 000000000..025144bd4 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_single_auth.yaml @@ -0,0 +1,29 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + enabled: true + modes: + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + clientId: "example-client-id" + issuerURI: "https://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" diff --git a/helm_chart/crds/mongodb.com_mongodb.yaml b/helm_chart/crds/mongodb.com_mongodb.yaml index 880e89fa4..01101d360 100644 --- a/helm_chart/crds/mongodb.com_mongodb.yaml +++ b/helm_chart/crds/mongodb.com_mongodb.yaml @@ -1566,7 +1566,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. diff --git a/helm_chart/crds/mongodb.com_mongodbmulticluster.yaml b/helm_chart/crds/mongodb.com_mongodbmulticluster.yaml index a3dfcc0a1..1be90e11c 100644 --- a/helm_chart/crds/mongodb.com_mongodbmulticluster.yaml +++ b/helm_chart/crds/mongodb.com_mongodbmulticluster.yaml @@ -826,7 +826,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. diff --git a/helm_chart/crds/mongodb.com_opsmanagers.yaml b/helm_chart/crds/mongodb.com_opsmanagers.yaml index 2c33c0e49..42d1597d8 100644 --- a/helm_chart/crds/mongodb.com_opsmanagers.yaml +++ b/helm_chart/crds/mongodb.com_opsmanagers.yaml @@ -888,7 +888,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. diff --git a/pkg/util/constants.go b/pkg/util/constants.go index d44b86223..b2082f6ee 100644 --- a/pkg/util/constants.go +++ b/pkg/util/constants.go @@ -140,6 +140,7 @@ const ( AutomationConfigLDAPOption = "PLAIN" AutomationConfigScramSha256Option = "SCRAM-SHA-256" AutomationConfigScramSha1Option = "MONGODB-CR" + AutomationConfigOIDCOption = "MONGODB-OIDC" AutomationAgentUserName = "mms-automation-agent" RequireClientCertificates = "REQUIRE" OptionalClientCertficates = "OPTIONAL" diff --git a/public/crds.yaml b/public/crds.yaml index 4fa8433ba..5ab7e45ae 100644 --- a/public/crds.yaml +++ b/public/crds.yaml @@ -1566,7 +1566,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. @@ -4204,7 +4203,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim. @@ -5853,7 +5851,6 @@ spec: pattern: ^[a-zA-Z0-9-_]+$ type: string groupsClaim: - default: groups description: |- The identifier of the claim that includes the principal's IdP user group membership information. Accept the default value unless your IdP uses a different claim, or you need a custom claim.