Skip to content

Commit 3a1acbc

Browse files
authored
Merge pull request #886 from NikSays/password-hash
Support PasswordHash in User type
2 parents 16f09c3 + 138c602 commit 3a1acbc

File tree

10 files changed

+316
-56
lines changed

10 files changed

+316
-56
lines changed

api/v1beta1/user_types.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ type UserSpec struct {
2727
// exist for the User object to be created.
2828
// +kubebuilder:validation:Required
2929
RabbitmqClusterReference RabbitmqClusterReference `json:"rabbitmqClusterReference"`
30-
// Defines a Secret used to pre-define the username and password set for this User. User objects created
31-
// with this field set will not have randomly-generated credentials, and will instead import
32-
// the username/password values from this Secret.
33-
// The Secret must contain the keys `username` and `password` in its Data field, or the import will fail.
34-
// Note that this import only occurs at creation time, and is ignored once a password has been set
35-
// on a User.
30+
// Defines a Secret containing the credentials for the User. If this field is omitted, random a username and
31+
// password will be generated. The Secret must have the following keys in its Data field:
32+
//
33+
// * `username` – Must be present or the import will fail.
34+
// * `passwordHash` – The SHA-512 hash of the password. If the hash is an empty string, a passwordless user
35+
// will be created. For more information, see https://www.rabbitmq.com/docs/passwords.
36+
// * `password` – Plain-text password. Will be used only if the `passwordHash` key is missing.
37+
//
38+
// Note that this import only occurs at creation time, and is ignored once a password has been set on a User.
3639
ImportCredentialsSecret *corev1.LocalObjectReference `json:"importCredentialsSecret,omitempty"`
3740
}
3841

config/crd/bases/rabbitmq.com_users.yaml

+11-6
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,17 @@ spec:
4343
properties:
4444
importCredentialsSecret:
4545
description: |-
46-
Defines a Secret used to pre-define the username and password set for this User. User objects created
47-
with this field set will not have randomly-generated credentials, and will instead import
48-
the username/password values from this Secret.
49-
The Secret must contain the keys `username` and `password` in its Data field, or the import will fail.
50-
Note that this import only occurs at creation time, and is ignored once a password has been set
51-
on a User.
46+
Defines a Secret containing the credentials for the User. If this field is omitted, random a username and
47+
password will be generated. The Secret must have the following keys in its Data field:
48+
49+
50+
* `username` – Must be present or the import will fail.
51+
* `passwordHash` – The SHA-512 hash of the password. If the hash is an empty string, a passwordless user
52+
will be created. For more information, see https://www.rabbitmq.com/docs/passwords.
53+
* `password` – Plain-text password. Will be used only if the `passwordHash` key is missing.
54+
55+
56+
Note that this import only occurs at creation time, and is ignored once a password has been set on a User.
5257
properties:
5358
name:
5459
default: ""

controllers/user_controller.go

+43-25
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,31 @@ type UserReconciler struct {
4848
func (r *UserReconciler) declareCredentials(ctx context.Context, user *topology.User) (string, error) {
4949
logger := ctrl.LoggerFrom(ctx)
5050

51-
username, password, err := r.generateCredentials(ctx, user)
51+
credentials, err := r.generateCredentials(ctx, user)
5252
if err != nil {
5353
logger.Error(err, "failed to generate credentials")
5454
return "", err
5555
}
56-
// Password wasn't in the provided input secret we need to generate a random one
57-
if password == "" {
58-
password, err = internal.RandomEncodedString(24)
56+
// Neither PasswordHash nor Password wasn't in the provided input secret we need to generate a random password
57+
if credentials.PasswordHash == nil && credentials.Password == "" {
58+
credentials.Password, err = internal.RandomEncodedString(24)
5959
if err != nil {
6060
return "", fmt.Errorf("failed to generate random password: %w", err)
6161
}
62-
6362
}
6463

65-
logger.Info("Credentials generated for User", "user", user.Name, "generatedUsername", username)
64+
logger.Info("Credentials generated for User", "user", user.Name, "generatedUsername", credentials.Username)
65+
66+
credentialSecretData := map[string][]byte{
67+
"username": []byte(credentials.Username),
68+
}
69+
if credentials.PasswordHash != nil {
70+
// Create `passwordHash` field only if necessary, to distinguish between an unset hash and an empty one
71+
credentialSecretData["passwordHash"] = []byte(*credentials.PasswordHash)
72+
} else {
73+
// Store password in the credential secret only if it will be used
74+
credentialSecretData["password"] = []byte(credentials.Password)
75+
}
6676

6777
credentialSecret := corev1.Secret{
6878
ObjectMeta: metav1.ObjectMeta{
@@ -72,10 +82,7 @@ func (r *UserReconciler) declareCredentials(ctx context.Context, user *topology.
7282
Type: corev1.SecretTypeOpaque,
7383
// The format of the generated Secret conforms to the Provisioned Service
7484
// type Spec. For more information, see https://k8s-service-bindings.github.io/spec/#provisioned-service.
75-
Data: map[string][]byte{
76-
"username": []byte(username),
77-
"password": []byte(password),
78-
},
85+
Data: credentialSecretData,
7986
}
8087

8188
var operationResult controllerutil.OperationResult
@@ -102,10 +109,10 @@ func (r *UserReconciler) declareCredentials(ctx context.Context, user *topology.
102109
}
103110

104111
logger.Info("Successfully declared credentials secret", "secret", credentialSecret.Name, "namespace", credentialSecret.Namespace)
105-
return username, nil
112+
return credentials.Username, nil
106113
}
107114

108-
func (r *UserReconciler) generateCredentials(ctx context.Context, user *topology.User) (string, string, error) {
115+
func (r *UserReconciler) generateCredentials(ctx context.Context, user *topology.User) (internal.UserCredentials, error) {
109116
logger := ctrl.LoggerFrom(ctx)
110117

111118
var err error
@@ -117,37 +124,48 @@ func (r *UserReconciler) generateCredentials(ctx context.Context, user *topology
117124
return r.importCredentials(ctx, user.Spec.ImportCredentialsSecret.Name, user.Namespace)
118125
}
119126

120-
username, err := internal.RandomEncodedString(24)
127+
credentials := internal.UserCredentials{}
128+
129+
credentials.Username, err = internal.RandomEncodedString(24)
121130
if err != nil {
122-
return "", "", fmt.Errorf("failed to generate random username: %w", err)
131+
return credentials, fmt.Errorf("failed to generate random username: %w", err)
123132
}
124-
password, err := internal.RandomEncodedString(24)
133+
credentials.Password, err = internal.RandomEncodedString(24)
125134
if err != nil {
126-
return "", "", fmt.Errorf("failed to generate random password: %w", err)
135+
return credentials, fmt.Errorf("failed to generate random password: %w", err)
127136
}
128-
return username, password, nil
137+
return credentials, nil
129138
}
130139

131-
func (r *UserReconciler) importCredentials(ctx context.Context, secretName, secretNamespace string) (string, string, error) {
140+
func (r *UserReconciler) importCredentials(ctx context.Context, secretName, secretNamespace string) (internal.UserCredentials, error) {
132141
logger := ctrl.LoggerFrom(ctx)
133142
logger.Info("Importing user credentials from provided Secret", "secretName", secretName, "secretNamespace", secretNamespace)
134143

144+
var credentials internal.UserCredentials
135145
var credentialsSecret corev1.Secret
146+
136147
err := r.Client.Get(ctx, types.NamespacedName{Name: secretName, Namespace: secretNamespace}, &credentialsSecret)
137148
if err != nil {
138-
return "", "", fmt.Errorf("could not find password secret %s in namespace %s; Err: %w", secretName, secretNamespace, err)
149+
return credentials, fmt.Errorf("could not find password secret %s in namespace %s; Err: %w", secretName, secretNamespace, err)
139150
}
151+
140152
username, ok := credentialsSecret.Data["username"]
141-
if !ok {
142-
return "", "", fmt.Errorf("could not find username key in credentials secret: %s", credentialsSecret.Name)
153+
if !ok || len(username) == 0 {
154+
return credentials, fmt.Errorf("could not find username key in credentials secret: %s", credentialsSecret.Name)
143155
}
144-
password, ok := credentialsSecret.Data["password"]
145-
if !ok {
146-
return string(username), "", nil
156+
credentials.Username = string(username)
157+
158+
password := credentialsSecret.Data["password"]
159+
credentials.Password = string(password)
160+
161+
passwordHash, ok := credentialsSecret.Data["passwordHash"]
162+
if ok {
163+
credentials.PasswordHash = new(string)
164+
*credentials.PasswordHash = string(passwordHash)
147165
}
148166

149167
logger.Info("Retrieved credentials from Secret", "secretName", secretName, "retrievedUsername", string(username))
150-
return string(username), string(password), nil
168+
return credentials, nil
151169
}
152170

153171
func (r *UserReconciler) setUserStatus(ctx context.Context, user *topology.User, username string) error {

docs/api/rabbitmq.com.ref.asciidoc

+11-6
Original file line numberDiff line numberDiff line change
@@ -1411,12 +1411,17 @@ but cannot perform any management actions.
14111411
For more information, see https://www.rabbitmq.com/management.html#permissions.
14121412
| *`rabbitmqClusterReference`* __xref:{anchor_prefix}-github-com-rabbitmq-messaging-topology-operator-api-v1beta1-rabbitmqclusterreference[$$RabbitmqClusterReference$$]__ | Reference to the RabbitmqCluster that the user will be created for. This cluster must
14131413
exist for the User object to be created.
1414-
| *`importCredentialsSecret`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | Defines a Secret used to pre-define the username and password set for this User. User objects created
1415-
with this field set will not have randomly-generated credentials, and will instead import
1416-
the username/password values from this Secret.
1417-
The Secret must contain the keys `username` and `password` in its Data field, or the import will fail.
1418-
Note that this import only occurs at creation time, and is ignored once a password has been set
1419-
on a User.
1414+
| *`importCredentialsSecret`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | Defines a Secret containing the credentials for the User. If this field is omitted, random a username and
1415+
password will be generated. The Secret must have the following keys in its Data field:
1416+
1417+
1418+
* `username` – Must be present or the import will fail.
1419+
* `passwordHash` – The SHA-512 hash of the password. If the hash is an empty string, a passwordless user
1420+
will be created. For more information, see https://www.rabbitmq.com/docs/passwords.
1421+
* `password` – Plain-text password. Will be used only if the `passwordHash` key is missing.
1422+
1423+
1424+
Note that this import only occurs at creation time, and is ignored once a password has been set on a User.
14201425
|===
14211426

14221427

docs/examples/users/README.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
# User examples
22

3-
This section contains 3 examples for creating RabbitMQ users.
4-
Messaging Topology Operator creates users with generated credentials by default. To create RabbitMQ users with provided credentials, you can reference a kubernetes secret object contains keys `username` and `password` in its Data field.
5-
See [userPreDefinedCreds.yaml](./userPreDefinedCreds.yaml) and [publish-consume-user.yaml](./publish-consume-user.yaml) as examples.
3+
This section contains the examples for creating RabbitMQ users.
4+
5+
Messaging Topology Operator creates users with generated credentials by default. To create RabbitMQ users with provided credentials, you can reference a kubernetes secret object with the following keys in its Data field:
6+
7+
* `username` – Must be present or the import will fail.
8+
* `passwordHash` – The SHA-512 hash of the password, as described in [RabbitMQ Docs](https://www.rabbitmq.com/docs/passwords). If the hash is an empty string, a passwordless user will be created.
9+
* `password` – Plain-text password. Will be used only if the `passwordHash` key is missing.
10+
11+
See [userPreDefinedCreds.yaml](./userPreDefinedCreds.yaml), [userWithPasswordHash.yaml](userWithPasswordHash.yaml), [passwordlessUser.yaml](passwordlessUser.yaml) and [publish-consume-user.yaml](./publish-consume-user.yaml) as examples.
12+
613
From [Messaging Topology Operator v1.10.0](https://github.com/rabbitmq/messaging-topology-operator/releases/tag/v1.10.1), you can provide a username and reply on the Operator to generate its password for you.
714
See [setUsernamewithGenPass.yaml](./setUsernamewithGenPass.yaml) as an example.
815

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: credentials-secret
5+
type: Opaque
6+
stringData:
7+
username: import-user-sample
8+
passwordHash: "" # The user will not have a valid password. Login attempts with any password will be rejected
9+
password: anythingreally # This value will be ignored, because `passwordHash` takes precedence
10+
---
11+
apiVersion: rabbitmq.com/v1beta1
12+
kind: User
13+
metadata:
14+
name: import-user-sample
15+
spec:
16+
tags:
17+
- management # available tags are 'management', 'policymaker', 'monitoring' and 'administrator'
18+
- policymaker
19+
rabbitmqClusterReference:
20+
name: test # rabbitmqCluster must exist in the same namespace as this resource
21+
importCredentialsSecret:
22+
name: credentials-secret
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: credentials-secret
5+
type: Opaque
6+
stringData:
7+
username: import-user-sample
8+
passwordHash: SjWbNXaNEwcoOOZWxG6J1HCF5P83lUavsCto+wh1s9zdOfoZ/CPv6l/SSdK3RC2+1QWmJGdYt5740j3ZLf/0RbpusNc= # SHA-512 hash of "some-password"
9+
---
10+
apiVersion: rabbitmq.com/v1beta1
11+
kind: User
12+
metadata:
13+
name: import-user-sample
14+
spec:
15+
tags:
16+
- management # available tags are 'management', 'policymaker', 'monitoring' and 'administrator'
17+
- policymaker
18+
rabbitmqClusterReference:
19+
name: test # rabbitmqCluster must exist in the same namespace as this resource
20+
importCredentialsSecret:
21+
name: credentials-secret

internal/user_settings.go renamed to internal/user.go

+28-9
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,37 @@ import (
1717
corev1 "k8s.io/api/core/v1"
1818
)
1919

20+
// UserCredentials describes the credentials that can be provided in ImportCredentialsSecret for a User.
21+
// If the secret is not provided, a random username and password will be generated.
22+
type UserCredentials struct {
23+
// Must be present if ImportCredentialsSecret is provided.
24+
Username string
25+
// If PasswordHash is an empty string, a passwordless user is created.
26+
// If PasswordHash is nil, Password is used instead.
27+
PasswordHash *string
28+
// If Password is empty and PasswordHash is nil, a random password is generated.
29+
Password string
30+
}
31+
2032
func GenerateUserSettings(credentials *corev1.Secret, tags []topology.UserTag) (rabbithole.UserSettings, error) {
2133
username, ok := credentials.Data["username"]
2234
if !ok {
2335
return rabbithole.UserSettings{}, fmt.Errorf("could not find username in credentials secret %s", credentials.Name)
2436
}
25-
password, ok := credentials.Data["password"]
37+
38+
passwordHash, ok := credentials.Data["passwordHash"]
2639
if !ok {
27-
return rabbithole.UserSettings{}, fmt.Errorf("could not find password in credentials secret %s", credentials.Name)
40+
// Use password as a fallback
41+
password, ok := credentials.Data["password"]
42+
if !ok {
43+
return rabbithole.UserSettings{}, fmt.Errorf("could not find passwordHash or password in credentials secret %s", credentials.Name)
44+
}
45+
// To avoid sending raw passwords over the wire, compute a password hash using a random salt
46+
// and use this in the UserSettings instead.
47+
// For more information on this hashing algorithm, see
48+
// https://www.rabbitmq.com/passwords.html#computing-password-hash.
49+
passwordHashStr := rabbithole.Base64EncodedSaltedPasswordHashSHA512(string(password))
50+
passwordHash = []byte(passwordHashStr)
2851
}
2952

3053
var userTagStrings []string
@@ -33,13 +56,9 @@ func GenerateUserSettings(credentials *corev1.Secret, tags []topology.UserTag) (
3356
}
3457

3558
return rabbithole.UserSettings{
36-
Name: string(username),
37-
Tags: userTagStrings,
38-
// To avoid sending raw passwords over the wire, compute a password hash using a random salt
39-
// and use this in the UserSettings instead.
40-
// For more information on this hashing algorithm, see
41-
// https://www.rabbitmq.com/passwords.html#computing-password-hash.
42-
PasswordHash: rabbithole.Base64EncodedSaltedPasswordHashSHA512(string(password)),
59+
Name: string(username),
60+
Tags: userTagStrings,
61+
PasswordHash: string(passwordHash),
4362
HashingAlgorithm: rabbithole.HashingAlgorithmSHA512,
4463
}, nil
4564
}

internal/user_settings_test.go renamed to internal/user_test.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ var _ = Describe("GenerateUserSettings", func() {
2727
userTags = []topology.UserTag{"administrator", "monitoring"}
2828
})
2929

30-
It("generates the expected rabbithole.UserSettings", func() {
30+
It("uses the password to generate the expected rabbithole.UserSettings", func() {
3131
settings, err := internal.GenerateUserSettings(&credentialSecret, userTags)
3232
Expect(err).NotTo(HaveOccurred())
3333
Expect(settings.Name).To(Equal("my-rabbit-user"))
3434
Expect(settings.Tags).To(ConsistOf("administrator", "monitoring"))
3535
Expect(settings.HashingAlgorithm.String()).To(Equal(rabbithole.HashingAlgorithmSHA512.String()))
3636

37+
// Password should not be sent, even if provided
38+
Expect(settings.Password).To(BeEmpty())
39+
3740
// The first 4 bytes of the PasswordHash will be the salt used in the hashing algorithm.
3841
// See https://www.rabbitmq.com/passwords.html#computing-password-hash.
3942
// We can take this salt and calculate what the correct hashed salted value would
@@ -45,4 +48,19 @@ var _ = Describe("GenerateUserSettings", func() {
4548
saltedHash := sha512.Sum512([]byte(string(salt) + "a-secure-password"))
4649
Expect(base64.StdEncoding.EncodeToString([]byte(string(salt) + string(saltedHash[:])))).To(Equal(settings.PasswordHash))
4750
})
51+
52+
It("uses the passwordHash to generate the expected rabbithole.UserSettings", func() {
53+
hash, _ := rabbithole.SaltedPasswordHashSHA256("a-different-password")
54+
credentialSecret.Data["passwordHash"] = []byte(hash)
55+
56+
settings, err := internal.GenerateUserSettings(&credentialSecret, userTags)
57+
Expect(err).NotTo(HaveOccurred())
58+
Expect(settings.Name).To(Equal("my-rabbit-user"))
59+
Expect(settings.Tags).To(ConsistOf("administrator", "monitoring"))
60+
Expect(settings.HashingAlgorithm.String()).To(Equal(rabbithole.HashingAlgorithmSHA512.String()))
61+
Expect(settings.PasswordHash).To(Equal(hash))
62+
63+
// Password should not be sent, even if provided
64+
Expect(settings.Password).To(BeEmpty())
65+
})
4866
})

0 commit comments

Comments
 (0)