Skip to content

Commit 57cca04

Browse files
[CLOUDP-349630] Don't generate scramshacreds if MongoDBUser's password is not changed (#628)
# Summary When the reconciliation happens for the `MongoDBUser` resource (because of controller restart or because of any other reason), for `SCRAM` authentication mechanism, the scram sha creds are generated and then they are set in the automation config which results into another version getting created for Automation Config. This workflow can be improved to generated the scram sha creds only when the password of the user is changed. If we do that we will not be changing the user's scram creds and the automation config will not be updated. To achieve this we are checking if the scram sha creds that are in automation config for a user, match with the scram sha creds generated with the stored salt and new password. If the scram sha creds match it means that the password for the user is not changed and we don't have to generate the new scram sha creds, otherwise new scram sha creds will be generated. ## Proof of Work Unit test: ``` ~/work/opensource/mongodb-kubernetes/controllers/operator/authentication (dont-rotate-scram-creds-mdbu) » go test -timeout 30s -run ^Test_isPasswordChanged$ -v === RUN Test_isPasswordChanged --- PASS: Test_isPasswordChanged (0.01s) PASS ok github.com/mongodb/mongodb-kubernetes/controllers/operator/authentication 0.472s ``` I am yet to do some more tests to actually be confident that the automation config is not changed when reconciliation happens. ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [x] Have you checked whether your jira ticket required DOCSP changes? - [x] Have you added changelog file? - use `skip-changelog` label if not needed - refer to [Changelog files and Release Notes](https://github.com/mongodb/mongodb-kubernetes/blob/master/CONTRIBUTING.md#changelog-files-and-release-notes) section in CONTRIBUTING.md for more details
1 parent 765c86c commit 57cca04

File tree

5 files changed

+168
-10
lines changed

5 files changed

+168
-10
lines changed

controllers/om/automation_config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,13 @@ type ScramShaCreds struct {
394394
StoredKey string `json:"storedKey"`
395395
}
396396

397+
func (s ScramShaCreds) Equals(i ScramShaCreds) bool {
398+
return s.IterationCount == i.IterationCount &&
399+
s.Salt == i.Salt &&
400+
s.ServerKey == i.ServerKey &&
401+
s.StoredKey == i.StoredKey
402+
}
403+
397404
func (u *MongoDBUser) AddRole(role *Role) {
398405
u.Roles = append(u.Roles, role)
399406
}

controllers/operator/authentication/scramsha_credentials.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,57 @@ const (
2828
rfc5802MandatedSaltSize = 4
2929
)
3030

31+
func isPasswordChanged(user *om.MongoDBUser, password string, acUser *om.MongoDBUser) (bool, error) {
32+
// if something unexpected happens return true to make sure that the scramShaCreds will be generated again.
33+
if acUser == nil || acUser.ScramSha256Creds == nil || acUser.ScramSha1Creds == nil {
34+
return true, nil
35+
}
36+
37+
if acUser.ScramSha256Creds.Salt != "" && acUser.ScramSha1Creds.Salt != "" {
38+
sha256Salt, err := base64.StdEncoding.DecodeString(acUser.ScramSha256Creds.Salt)
39+
if err != nil {
40+
return true, err
41+
}
42+
sha1Salt, err := base64.StdEncoding.DecodeString(acUser.ScramSha1Creds.Salt)
43+
if err != nil {
44+
return true, nil
45+
}
46+
// generate scramshacreds with (new) password but with old salt to verify if given password
47+
// is actually changed
48+
newScramSha256Creds, err := computeScramShaCreds(user.Username, password, sha256Salt, ScramSha256)
49+
if err != nil {
50+
return false, xerrors.Errorf("error generating scramSha256 creds to verify with already present user on automation config %w", err)
51+
}
52+
53+
newScramSha1Creds, err := computeScramShaCreds(user.Username, password, sha1Salt, MongoDBCR)
54+
if err != nil {
55+
return false, xerrors.Errorf("error generating scramSha1 creds to verify with already present user on automation config %w", err)
56+
}
57+
return !newScramSha256Creds.Equals(*acUser.ScramSha256Creds) || !newScramSha1Creds.Equals(*acUser.ScramSha1Creds), nil
58+
}
59+
60+
return true, nil
61+
}
62+
3163
// ConfigureScramCredentials creates both SCRAM-SHA-1 and SCRAM-SHA-256 credentials. This ensures
3264
// that changes to the authentication settings on the MongoDB resources won't leave MongoDBUsers without
3365
// the correct credentials.
34-
func ConfigureScramCredentials(user *om.MongoDBUser, password string) error {
66+
func ConfigureScramCredentials(user *om.MongoDBUser, password string, ac *om.AutomationConfig) error {
67+
// there are chances that the reconciliation is happening again for this user resource and we wouldn't
68+
// want to generate scram creds again if the password of the user has not changed.
69+
_, acUser := ac.Auth.GetUser(user.Username, user.Database)
70+
changed, err := isPasswordChanged(user, password, acUser)
71+
if err != nil {
72+
return err
73+
}
74+
if !changed {
75+
// since the scram creds generated using the old salt are same with the scram creds stored in automation config
76+
// there is no need to generate new salt/creds.
77+
user.ScramSha256Creds = acUser.ScramSha256Creds
78+
user.ScramSha1Creds = acUser.ScramSha1Creds
79+
return nil
80+
}
81+
3582
scram256Salt, err := generateSalt(sha256.New)
3683
if err != nil {
3784
return xerrors.Errorf("error generating scramSha256 salt: %w", err)

controllers/operator/authentication/scramsha_credentials_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"testing"
77

88
"github.com/stretchr/testify/assert"
9+
10+
"github.com/mongodb/mongodb-kubernetes/controllers/om"
911
)
1012

1113
func TestScramSha1SecretsMatch(t *testing.T) {
@@ -23,3 +25,41 @@ func assertSecretsMatch(t *testing.T, hash func() hash.Hash, passwordHash string
2325
assert.Equal(t, computedStoredKey, storedKey)
2426
assert.Equal(t, computedServerKey, serverKey)
2527
}
28+
29+
var testAutomationConfig = &om.AutomationConfig{
30+
Auth: &om.Auth{
31+
Users: make([]*om.MongoDBUser, 0),
32+
},
33+
}
34+
35+
func Test_isPasswordChanged(t *testing.T) {
36+
userPassword := "secretpassword"
37+
userNewPassword := "newsecretpassword"
38+
39+
mongoUser := om.MongoDBUser{
40+
Username: "new-user",
41+
Database: "admin",
42+
}
43+
44+
// will generate scram creds for the user mongoUser and set it in its fields
45+
ConfigureScramCredentials(&mongoUser, userPassword, testAutomationConfig)
46+
47+
testAutomationConfig.Auth.Users = append(testAutomationConfig.Auth.Users, &om.MongoDBUser{
48+
Username: mongoUser.Username,
49+
Database: mongoUser.Database,
50+
ScramSha256Creds: mongoUser.ScramSha256Creds,
51+
ScramSha1Creds: mongoUser.ScramSha1Creds,
52+
})
53+
54+
// now that the scram creds are set in the automation config, let's say the reconciliation happens again
55+
// with the same user and same password, isPasswordChanged should return false
56+
_, u := testAutomationConfig.Auth.GetUser(mongoUser.Username, mongoUser.Database)
57+
op, err := isPasswordChanged(&mongoUser, userPassword, u)
58+
assert.Nil(t, err)
59+
assert.False(t, op)
60+
61+
// if reconciliation happens again with diff password, isPasswordChanged should return true
62+
op, err = isPasswordChanged(&mongoUser, userNewPassword, u)
63+
assert.Nil(t, err)
64+
assert.True(t, op)
65+
}

controllers/operator/mongodbuser_controller.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ func AddMongoDBUserController(ctx context.Context, mgr manager.Manager, memberCl
333333
// toOmUser converts a MongoDBUser specification and optional password into an
334334
// automation config MongoDB user. If the user has no password then a blank
335335
// password should be provided.
336-
func toOmUser(spec userv1.MongoDBUserSpec, password string) (om.MongoDBUser, error) {
336+
func toOmUser(spec userv1.MongoDBUserSpec, password string, ac *om.AutomationConfig) (om.MongoDBUser, error) {
337337
user := om.MongoDBUser{
338338
Database: spec.Database,
339339
Username: spec.Username,
@@ -344,7 +344,7 @@ func toOmUser(spec userv1.MongoDBUserSpec, password string) (om.MongoDBUser, err
344344

345345
// only specify password if we're dealing with non-x509 users
346346
if spec.Database != authentication.ExternalDB {
347-
if err := authentication.ConfigureScramCredentials(&user, password); err != nil {
347+
if err := authentication.ConfigureScramCredentials(&user, password, ac); err != nil {
348348
return om.MongoDBUser{}, xerrors.Errorf("error generating SCRAM credentials: %w", err)
349349
}
350350
}
@@ -385,7 +385,7 @@ func (r *MongoDBUserReconciler) handleScramShaUser(ctx context.Context, user *us
385385
auth.RemoveUser(user.Status.Username, user.Status.Database)
386386
}
387387

388-
desiredUser, err := toOmUser(user.Spec, password)
388+
desiredUser, err := toOmUser(user.Spec, password, ac)
389389
if err != nil {
390390
return err
391391
}
@@ -421,18 +421,18 @@ func (r *MongoDBUserReconciler) handleScramShaUser(ctx context.Context, user *us
421421
}
422422

423423
func (r *MongoDBUserReconciler) handleExternalAuthUser(ctx context.Context, user *userv1.MongoDBUser, conn om.Connection, log *zap.SugaredLogger) (reconcile.Result, error) {
424-
desiredUser, err := toOmUser(user.Spec, "")
425-
if err != nil {
426-
return r.updateStatus(ctx, user, workflow.Failed(xerrors.Errorf("error updating user %w", err)), log)
427-
}
428-
429424
shouldRetry := false
430425
updateFunction := func(ac *om.AutomationConfig) error {
431426
if !externalAuthMechanismsAvailable(ac.Auth.DeploymentAuthMechanisms) {
432427
shouldRetry = true
433428
return xerrors.Errorf("no external authentication mechanisms (LDAP or x509) have been configured")
434429
}
435430

431+
desiredUser, err := toOmUser(user.Spec, "", ac)
432+
if err != nil {
433+
return xerrors.Errorf("errorr updating user %w", err)
434+
}
435+
436436
auth := ac.Auth
437437
if user.ChangedIdentifier() {
438438
auth.RemoveUser(user.Status.Username, user.Status.Database)
@@ -442,7 +442,7 @@ func (r *MongoDBUserReconciler) handleExternalAuthUser(ctx context.Context, user
442442
return nil
443443
}
444444

445-
err = conn.ReadUpdateAutomationConfig(updateFunction, log)
445+
err := conn.ReadUpdateAutomationConfig(updateFunction, log)
446446
if err != nil {
447447
if shouldRetry {
448448
return r.updateStatus(ctx, user, workflow.Pending("%s", err.Error()).WithRetry(10), log)

controllers/operator/mongodbuser_controller_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package operator
22

33
import (
44
"context"
5+
"encoding/json"
56
"testing"
67
"time"
78

@@ -71,6 +72,69 @@ func TestUserIsAdded_ToAutomationConfig_OnSuccessfulReconciliation(t *testing.T)
7172
assert.Equal(t, len(user.Spec.Roles), len(createdUser.Roles))
7273
}
7374

75+
// Not making this (DeepCopy) a method of type AutomationConfig because I want to be
76+
// explicit that this method is just for test code.
77+
func DeepCopy(original *om.AutomationConfig) (*om.AutomationConfig, error) {
78+
if original == nil {
79+
return nil, nil
80+
}
81+
82+
b, err := json.Marshal(original)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
var newAC om.AutomationConfig
88+
err = json.Unmarshal(b, &newAC)
89+
if err != nil {
90+
return nil, err
91+
}
92+
return &newAC, nil
93+
}
94+
95+
func TestNoChange_InAC_After_Same_User_Reconciliation(t *testing.T) {
96+
ctx := context.Background()
97+
user := DefaultMongoDBUserBuilder().SetMongoDBResourceName("my-rs").Build()
98+
reconciler, client, omConnectionFactory := userReconcilerWithAuthMode(ctx, user, util.AutomationConfigScramSha256Option)
99+
100+
// initialize resources required for the tests
101+
_ = client.Create(ctx, DefaultReplicaSetBuilder().EnableAuth().AgentAuthMode("SCRAM").
102+
SetName("my-rs").Build())
103+
createUserControllerConfigMap(ctx, client)
104+
createPasswordSecret(ctx, client, user.Spec.PasswordSecretKeyRef, "password")
105+
106+
actual, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: kube.ObjectKey(user.Namespace, user.Name)})
107+
108+
okReconcileResult, _ := workflow.OK().ReconcileResult()
109+
110+
assert.Nil(t, err, "there should be no error on successful reconciliation")
111+
assert.Equal(t, okReconcileResult, actual, "there should be a successful reconciliation if the password is a valid reference")
112+
113+
ac, _ := omConnectionFactory.GetConnection().ReadAutomationConfig()
114+
// since underlying implmentation of ReadAutomationConfig just has reference to automation config
115+
// it's better to deep copy this version of AC so that it can be used to compare later.
116+
originalAC, err := DeepCopy(ac)
117+
assert.Nil(t, err)
118+
119+
// the automation config should have been updated during reconciliation
120+
assert.Len(t, ac.Auth.Users, 1, "the MongoDBUser should have been added to the AutomationConfig")
121+
122+
_, createdUser := ac.Auth.GetUser("my-user", "admin")
123+
assert.Equal(t, user.Spec.Username, createdUser.Username)
124+
assert.Equal(t, user.Spec.Database, createdUser.Database)
125+
assert.Equal(t, len(user.Spec.Roles), len(createdUser.Roles))
126+
127+
// reconcile the same user again and make sure the automation config is not updated, if the user's password is not changed
128+
// we don't generate scram creds and because of that we wouldn't see changes in automatino config
129+
reconcileResult, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: kube.ObjectKey(user.Namespace, user.Name)})
130+
assert.Nil(t, err)
131+
assert.Equal(t, okReconcileResult, reconcileResult)
132+
133+
acAfterSecondReconcile, _ := omConnectionFactory.GetConnection().ReadAutomationConfig()
134+
// verify that the automation cnofig has not been changed because we ran reconciliation second time with the same user
135+
assert.True(t, acAfterSecondReconcile.EqualsWithoutDeployment(*originalAC), "Automation config before the second reconciliation and after the second reconciliation should be same")
136+
}
137+
74138
func TestReconciliationSucceed_OnAddingUser_FromADifferentNamespace(t *testing.T) {
75139
ctx := context.Background()
76140
user := DefaultMongoDBUserBuilder().SetMongoDBResourceName("my-rs").Build()

0 commit comments

Comments
 (0)