Skip to content

Commit 03deb30

Browse files
committed
Add user limits.
1 parent 870860b commit 03deb30

13 files changed

+458
-1
lines changed

api/v1beta1/user_types.go

+16
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ type UserSpec struct {
3737
//
3838
// Note that this import only occurs at creation time, and is ignored once a password has been set on a User.
3939
ImportCredentialsSecret *corev1.LocalObjectReference `json:"importCredentialsSecret,omitempty"`
40+
// Limits to apply to a user to restrict the number of connections and channels
41+
// the user can create. These limits can be used as guard rails in environments
42+
// where applications cannot be trusted and monitored in detail, for example,
43+
// when RabbitMQ clusters are offered as a service. See https://www.rabbitmq.com/docs/user-limits.
44+
UserLimits UserLimits `json:"limits,omitempty"`
4045
}
4146

4247
// UserStatus defines the observed state of User.
@@ -56,6 +61,17 @@ type UserStatus struct {
5661
// +kubebuilder:validation:Enum=management;policymaker;monitoring;administrator
5762
type UserTag string
5863

64+
// Limits to apply to a user to restrict the number of connections and channels
65+
// the user can create. These limits can be used as guard rails in environments
66+
// where applications cannot be trusted and monitored in detail, for example,
67+
// when RabbitMQ clusters are offered as a service. See https://www.rabbitmq.com/docs/user-limits.
68+
type UserLimits struct {
69+
// Limits how many connections the user can open.
70+
Connections int32 `json:"connections,omitempty"`
71+
// Limits how many AMQP 0.9.1 channels the user can open.
72+
Channels int32 `json:"channels,omitempty"`
73+
}
74+
5975
// +genclient
6076
// +kubebuilder:object:root=true
6177
// +kubebuilder:resource:categories=rabbitmq

api/v1beta1/user_types_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,46 @@ var _ = Describe("user spec", func() {
9292
})
9393
})
9494

95+
When("creating a user with limits", func() {
96+
var user User
97+
var username string
98+
var userLimits UserLimits
99+
100+
JustBeforeEach(func() {
101+
user = User{
102+
ObjectMeta: metav1.ObjectMeta{
103+
Name: username,
104+
Namespace: namespace,
105+
},
106+
Spec: UserSpec{
107+
RabbitmqClusterReference: RabbitmqClusterReference{
108+
Name: "some-cluster",
109+
},
110+
UserLimits: userLimits,
111+
},
112+
}
113+
})
114+
115+
When("creating a user with valid limits", func() {
116+
BeforeEach(func() {
117+
username = "limits-user"
118+
userLimits = UserLimits{
119+
Connections: 5,
120+
Channels: 10,
121+
}
122+
})
123+
It("successfully creates the user", func() {
124+
Expect(k8sClient.Create(ctx, &user)).To(Succeed())
125+
fetchedUser := &User{}
126+
Expect(k8sClient.Get(ctx, types.NamespacedName{
127+
Name: user.Name,
128+
Namespace: user.Namespace,
129+
}, fetchedUser)).To(Succeed())
130+
Expect(fetchedUser.Spec.RabbitmqClusterReference).To(Equal(RabbitmqClusterReference{
131+
Name: "some-cluster",
132+
}))
133+
Expect(fetchedUser.Spec.UserLimits).To(Equal(UserLimits{Connections: 5, Channels: 10}))
134+
})
135+
})
136+
})
95137
})

api/v1beta1/zz_generated.deepcopy.go

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/rabbitmq.com_users.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ spec:
6464
type: string
6565
type: object
6666
x-kubernetes-map-type: atomic
67+
limits:
68+
description: |-
69+
Limits to apply to a user to restrict the number of connections and channels
70+
the user can create. These limits can be used as guard rails in environments
71+
where applications cannot be trusted and monitored in detail, for example,
72+
when RabbitMQ clusters are offered as a service. See https://www.rabbitmq.com/docs/user-limits.
73+
properties:
74+
channels:
75+
description: Limits how many AMQP 0.9.1 channels the user can
76+
open.
77+
format: int32
78+
type: integer
79+
connections:
80+
description: Limits how many connections the user can open.
81+
format: int32
82+
type: integer
83+
type: object
6784
rabbitmqClusterReference:
6885
description: |-
6986
Reference to the RabbitmqCluster that the user will be created for. This cluster must

controllers/permission_controller_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,14 @@ var _ = Describe("permission-controller", func() {
261261
Status: "204 No Content",
262262
StatusCode: http.StatusNoContent,
263263
}, nil)
264+
fakeRabbitMQClient.PutUserLimitsReturns(&http.Response{
265+
Status: "201 Created",
266+
StatusCode: http.StatusCreated,
267+
}, nil)
268+
fakeRabbitMQClient.DeleteUserLimitsReturns(&http.Response{
269+
Status: "204 No Content",
270+
StatusCode: http.StatusNoContent,
271+
}, nil)
264272
})
265273

266274
Context("creation", func() {

controllers/user_controller.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,14 @@ func (r *UserReconciler) DeclareFunc(ctx context.Context, client rabbitmqclient.
221221
}
222222
logger.Info("Generated user settings", "user", user.Name, "settings", userSettings)
223223

224-
return validateResponse(client.PutUser(userSettings.Name, userSettings))
224+
err = validateResponse(client.PutUser(userSettings.Name, userSettings))
225+
if err != nil {
226+
return err
227+
}
228+
229+
userLimits := internal.GenerateUserLimits(user.Spec.UserLimits)
230+
logger.Info("Generated user limits", "user", user.Name, "limits", userLimits)
231+
return validateResponse(client.PutUserLimits(user.Name, userLimits))
225232
}
226233

227234
func (r *UserReconciler) getUserCredentials(ctx context.Context, user *topology.User) (*corev1.Secret, error) {
@@ -245,5 +252,13 @@ func (r *UserReconciler) DeleteFunc(ctx context.Context, client rabbitmqclient.C
245252
} else if err != nil {
246253
return err
247254
}
255+
256+
userLimits := []string{"max-connections", "max-channels"}
257+
err = validateResponseForDeletion(client.DeleteUserLimits(user.Status.Username, userLimits))
258+
if errors.Is(err, NotFound) {
259+
logger.Info("cannot find user limits in rabbitmq server; already deleted", "user", user.Name, "limits", userLimits)
260+
} else if err != nil {
261+
return err
262+
}
248263
return nil
249264
}

controllers/user_controller_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var _ = Describe("UserController", func() {
3333
managerCtx context.Context
3434
managerCancel context.CancelFunc
3535
k8sClient runtimeClient.Client
36+
userLimits topology.UserLimits
3637
)
3738

3839
BeforeEach(func() {
@@ -91,6 +92,7 @@ var _ = Describe("UserController", func() {
9192
RabbitmqClusterReference: topology.RabbitmqClusterReference{
9293
Name: "example-rabbit",
9394
},
95+
UserLimits: userLimits,
9496
},
9597
}
9698
})
@@ -154,6 +156,54 @@ var _ = Describe("UserController", func() {
154156
})))
155157
})
156158
})
159+
160+
When("the user has limits defined", func() {
161+
BeforeEach(func() {
162+
fakeRabbitMQClient.PutUserReturns(&http.Response{
163+
Status: "201 Created",
164+
StatusCode: http.StatusCreated,
165+
}, nil)
166+
fakeRabbitMQClient.PutUserLimitsReturns(&http.Response{
167+
Status: "201 Created",
168+
StatusCode: http.StatusCreated,
169+
}, nil)
170+
userName = "limits-user"
171+
userLimits = topology.UserLimits{
172+
Connections: 5,
173+
Channels: 10,
174+
}
175+
})
176+
177+
It("should create the user limits", func() {
178+
Expect(k8sClient.Create(ctx, &user)).To(Succeed())
179+
Eventually(func() []topology.Condition {
180+
_ = k8sClient.Get(
181+
ctx,
182+
types.NamespacedName{Name: user.Name, Namespace: user.Namespace},
183+
&user,
184+
)
185+
186+
return user.Status.Conditions
187+
}).
188+
Within(statusEventsUpdateTimeout).
189+
WithPolling(time.Second).
190+
Should(ContainElement(MatchFields(IgnoreExtras, Fields{
191+
"Type": Equal(topology.ConditionType("Ready")),
192+
"Reason": Equal("SuccessfulCreateOrUpdate"),
193+
"Status": Equal(corev1.ConditionTrue),
194+
})))
195+
By("calling PutUserLimits with the correct user limits")
196+
Expect(fakeRabbitMQClient.PutUserLimitsCallCount()).To(BeNumerically(">", 0))
197+
username, userLimitsValues := fakeRabbitMQClient.PutUserLimitsArgsForCall(0)
198+
Expect(username).To(Equal(userName))
199+
connectionLimit, ok := userLimitsValues["max-connections"]
200+
Expect(ok).To(BeTrue())
201+
Expect(connectionLimit).To(Equal(5))
202+
channelLimit, ok := userLimitsValues["max-channels"]
203+
Expect(ok).To(BeTrue())
204+
Expect(channelLimit).To(Equal(10))
205+
})
206+
})
157207
})
158208

159209
When("deleting a user", func() {
@@ -162,6 +212,14 @@ var _ = Describe("UserController", func() {
162212
Status: "201 Created",
163213
StatusCode: http.StatusCreated,
164214
}, nil)
215+
fakeRabbitMQClient.PutUserLimitsReturns(&http.Response{
216+
Status: "201 Created",
217+
StatusCode: http.StatusCreated,
218+
}, nil)
219+
fakeRabbitMQClient.DeleteUserLimitsReturns(&http.Response{
220+
Status: "204 No Content",
221+
StatusCode: http.StatusNoContent,
222+
}, nil)
165223
Expect(k8sClient.Create(ctx, &user)).To(Succeed())
166224
Eventually(func() []topology.Condition {
167225
_ = k8sClient.Get(

docs/api/rabbitmq.com.ref.asciidoc

+25
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,27 @@ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-
13661366
|===
13671367

13681368

1369+
[id="{anchor_prefix}-github-com-rabbitmq-messaging-topology-operator-api-v1beta1-userlimits"]
1370+
==== UserLimits
1371+
1372+
Limits to apply to a user to restrict the number of connections and channels
1373+
the user can create. These limits can be used as guard rails in environments
1374+
where applications cannot be trusted and monitored in detail, for example,
1375+
when RabbitMQ clusters are offered as a service. See https://www.rabbitmq.com/docs/user-limits.
1376+
1377+
.Appears In:
1378+
****
1379+
- xref:{anchor_prefix}-github-com-rabbitmq-messaging-topology-operator-api-v1beta1-userspec[$$UserSpec$$]
1380+
****
1381+
1382+
[cols="25a,75a", options="header"]
1383+
|===
1384+
| Field | Description
1385+
| *`connections`* __integer__ | Limits how many connections the user can open.
1386+
| *`channels`* __integer__ | Limits how many AMQP 0.9.1 channels the user can open.
1387+
|===
1388+
1389+
13691390
[id="{anchor_prefix}-github-com-rabbitmq-messaging-topology-operator-api-v1beta1-userlist"]
13701391
==== UserList
13711392

@@ -1422,6 +1443,10 @@ password will be generated. The Secret must have the following keys in its Data
14221443
* `password` – Plain-text password. Will be used only if the `passwordHash` key is missing.
14231444

14241445
Note that this import only occurs at creation time, and is ignored once a password has been set on a User.
1446+
| *`limits`* __xref:{anchor_prefix}-github-com-rabbitmq-messaging-topology-operator-api-v1beta1-userlimits[$$UserLimits$$]__ | Limits to apply to a user to restrict the number of connections and channels
1447+
the user can create. These limits can be used as guard rails in environments
1448+
where applications cannot be trusted and monitored in detail, for example,
1449+
when RabbitMQ clusters are offered as a service. See https://www.rabbitmq.com/docs/user-limits.
14251450
|===
14261451

14271452

internal/user.go

+11
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,14 @@ func GenerateUserSettings(credentials *corev1.Secret, tags []topology.UserTag) (
6262
HashingAlgorithm: rabbithole.HashingAlgorithmSHA512,
6363
}, nil
6464
}
65+
66+
func GenerateUserLimits(userLimits topology.UserLimits) rabbithole.UserLimitsValues {
67+
userLimitsValues := rabbithole.UserLimitsValues{}
68+
if userLimits.Connections > 0 {
69+
userLimitsValues["max-connections"] = int(userLimits.Connections)
70+
}
71+
if userLimits.Channels > 0 {
72+
userLimitsValues["max-channels"] = int(userLimits.Channels)
73+
}
74+
return userLimitsValues
75+
}

internal/user_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,31 @@ var _ = Describe("GenerateUserSettings", func() {
6363
// Password should not be sent, even if provided
6464
Expect(settings.Password).To(BeEmpty())
6565
})
66+
67+
When("user limits are provided", func() {
68+
It("uses the limits to generate the expected rabbithole.UserLimits", func() {
69+
limits := internal.GenerateUserLimits(topology.UserLimits{
70+
Connections: 3,
71+
Channels: 7,
72+
})
73+
Expect(limits["max-connections"]).To(Equal(3))
74+
Expect(limits["max-channels"]).To(Equal(7))
75+
})
76+
77+
It("does not create unspecified limits", func() {
78+
limits := internal.GenerateUserLimits(topology.UserLimits{
79+
Connections: 5,
80+
})
81+
Expect(limits["max-connections"]).To(Equal(5))
82+
_, ok := limits["max-channels"]
83+
Expect(ok).To(BeFalse())
84+
})
85+
})
86+
87+
When("no user limits are provided", func() {
88+
It("does not specify limits", func() {
89+
limits := internal.GenerateUserLimits(topology.UserLimits{})
90+
Expect(limits).To(BeEmpty())
91+
})
92+
})
6693
})

rabbitmqclient/rabbitmq_client_factory.go

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
type Client interface {
2323
PutUser(string, rabbithole.UserSettings) (*http.Response, error)
2424
DeleteUser(string) (*http.Response, error)
25+
PutUserLimits(string, rabbithole.UserLimitsValues) (*http.Response, error)
26+
DeleteUserLimits(string, rabbithole.UserLimits) (*http.Response, error)
2527
DeclareBinding(string, rabbithole.BindingInfo) (*http.Response, error)
2628
DeleteBinding(string, rabbithole.BindingInfo) (*http.Response, error)
2729
ListQueueBindingsBetween(string, string, string) ([]rabbithole.BindingInfo, error)

0 commit comments

Comments
 (0)