Skip to content

Commit 92e525f

Browse files
authored
[v0.8] Introduce username validation (#1023) (#1026)
* Add update validation for User.UserName (#943) * Add update validation for User.UserName * Fix unit test * Move username validation to happen before manage-users check (#1016)
1 parent 5dd9c0d commit 92e525f

File tree

4 files changed

+106
-29
lines changed

4 files changed

+106
-29
lines changed

docs.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,12 @@ When a user is updated or deleted, a check occurs to ensure that the user making
356356

357357
If the user making the request has the verb `manage-users` for the resource `users`, then it is allowed to bypass the check. Note that the wildcard `*` includes the `manage-users` verb.
358358

359+
#### Invalid Fields - Update
360+
361+
Users can update the following fields if they had not been set. But after getting initial values, the fields cannot be changed:
362+
363+
- UserName
364+
359365
## UserAttribute
360366

361367
### Validation Checks

pkg/resources/management.cattle.io/v3/users/User.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@
44

55
When a user is updated or deleted, a check occurs to ensure that the user making the request has permissions greater than or equal to the user being updated or deleted. To get the user's groups, the user's UserAttributes are checked. This is best effort, because UserAttributes are only updated when a User logs in, so it may not be perfectly up to date.
66

7-
If the user making the request has the verb `manage-users` for the resource `users`, then it is allowed to bypass the check. Note that the wildcard `*` includes the `manage-users` verb.
7+
If the user making the request has the verb `manage-users` for the resource `users`, then it is allowed to bypass the check. Note that the wildcard `*` includes the `manage-users` verb.
8+
9+
### Invalid Fields - Update
10+
11+
Users can update the following fields if they had not been set. But after getting initial values, the fields cannot be changed:
12+
13+
- UserName

pkg/resources/management.cattle.io/v3/users/validator.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
apierrors "k8s.io/apimachinery/pkg/api/errors"
1414
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1515
"k8s.io/apimachinery/pkg/runtime/schema"
16+
"k8s.io/apimachinery/pkg/util/validation/field"
1617
"k8s.io/apiserver/pkg/authentication/user"
1718
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
1819
"k8s.io/kubernetes/pkg/registry/rbac/validation"
@@ -71,6 +72,19 @@ func (v *Validator) Admitters() []admission.Admitter {
7172
}
7273

7374
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
75+
oldUser, newUser, err := objectsv3.UserOldAndNewFromRequest(&request.AdmissionRequest)
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to get current User from request: %w", err)
78+
}
79+
80+
// Validate update fields
81+
fieldPath := field.NewPath("user")
82+
if request.Operation == admissionv1.Update {
83+
if err := validateUpdateFields(oldUser, newUser, fieldPath); err != nil {
84+
return admission.ResponseBadRequest(err.Error()), nil
85+
}
86+
}
87+
7488
// Check if requester has manage-user verb
7589
hasManageUsers, err := auth.RequestUserHasVerb(request, gvr, a.sar, manageUsersVerb, "", "")
7690
if err != nil {
@@ -80,26 +94,22 @@ func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResp
8094
if hasManageUsers {
8195
return &admissionv1.AdmissionResponse{Allowed: true}, nil
8296
}
83-
userObj, err := objectsv3.UserFromRequest(&request.AdmissionRequest)
84-
if err != nil {
85-
return nil, fmt.Errorf("failed to get current User from request: %w", err)
86-
}
8797

8898
// Need the UserAttribute to find the groups
89-
userAttribute, err := a.userAttributeCache.Get(userObj.Name)
99+
userAttribute, err := a.userAttributeCache.Get(oldUser.Name)
90100
if err != nil && !apierrors.IsNotFound(err) {
91-
return nil, fmt.Errorf("failed to get UserAttribute for %s: %w", userObj.Name, err)
101+
return nil, fmt.Errorf("failed to get UserAttribute for %s: %w", oldUser.Name, err)
92102
}
93103

94104
userInfo := &user.DefaultInfo{
95-
Name: userObj.Name,
105+
Name: oldUser.Name,
96106
Groups: getGroupsFromUserAttribute(userAttribute),
97107
}
98108

99109
// Get all rules for the user being modified
100110
rules, err := a.resolver.RulesFor(userInfo, "")
101111
if err != nil {
102-
return nil, fmt.Errorf("failed to get rules for user %v: %w", userObj, err)
112+
return nil, fmt.Errorf("failed to get rules for user %v: %w", oldUser, err)
103113
}
104114

105115
// Ensure that rules of the user being modified aren't greater than the rules of the user making the request
@@ -132,3 +142,12 @@ func getGroupsFromUserAttribute(userAttribute *v3.UserAttribute) []string {
132142
}
133143
return result
134144
}
145+
146+
// validateUpdateFields validates fields during an update. The manage-users verb does not apply to these validations.
147+
func validateUpdateFields(oldUser, newUser *v3.User, fieldPath *field.Path) error {
148+
const reason = "field is immutable"
149+
if oldUser.Username != "" && oldUser.Username != newUser.Username {
150+
return field.Invalid(fieldPath.Child("username"), newUser.Username, reason)
151+
}
152+
return nil
153+
}

pkg/resources/management.cattle.io/v3/users/validator_test.go

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var (
3535
ObjectMeta: metav1.ObjectMeta{
3636
Name: defaultUserName,
3737
},
38+
Username: defaultUserName,
3839
}
3940
getPods = []rbacv1.PolicyRule{
4041
{
@@ -114,12 +115,12 @@ func Test_Admit(t *testing.T) {
114115
oldUser: defaultUser.DeepCopy(),
115116
requestUserName: requesterUserName,
116117
resolverRulesFor: func(s string) ([]rbacv1.PolicyRule, error) {
117-
if s == requesterUserName {
118-
return getPods, nil
119-
} else if s == defaultUserName {
118+
switch s {
119+
case requesterUserName, defaultUserName:
120120
return getPods, nil
121+
default:
122+
return nil, fmt.Errorf("unexpected error")
121123
}
122-
return nil, fmt.Errorf("unexpected error")
123124
},
124125
allowed: true,
125126
},
@@ -129,12 +130,12 @@ func Test_Admit(t *testing.T) {
129130
newUser: defaultUser.DeepCopy(),
130131
requestUserName: requesterUserName,
131132
resolverRulesFor: func(s string) ([]rbacv1.PolicyRule, error) {
132-
if s == requesterUserName {
133-
return getPods, nil
134-
} else if s == defaultUserName {
133+
switch s {
134+
case requesterUserName, defaultUserName:
135135
return getPods, nil
136+
default:
137+
return nil, fmt.Errorf("unexpected error")
136138
}
137-
return nil, fmt.Errorf("unexpected error")
138139
},
139140
allowed: true,
140141
},
@@ -143,12 +144,14 @@ func Test_Admit(t *testing.T) {
143144
oldUser: defaultUser.DeepCopy(),
144145
requestUserName: requesterUserName,
145146
resolverRulesFor: func(s string) ([]rbacv1.PolicyRule, error) {
146-
if s == requesterUserName {
147+
switch s {
148+
case requesterUserName:
147149
return starPods, nil
148-
} else if s == defaultUserName {
150+
case defaultUserName:
149151
return getPods, nil
152+
default:
153+
return nil, fmt.Errorf("unexpected error")
150154
}
151-
return nil, fmt.Errorf("unexpected error")
152155
},
153156
allowed: true,
154157
},
@@ -158,12 +161,14 @@ func Test_Admit(t *testing.T) {
158161
newUser: defaultUser.DeepCopy(),
159162
requestUserName: requesterUserName,
160163
resolverRulesFor: func(s string) ([]rbacv1.PolicyRule, error) {
161-
if s == requesterUserName {
164+
switch s {
165+
case requesterUserName:
162166
return starPods, nil
163-
} else if s == defaultUserName {
167+
case defaultUserName:
164168
return getPods, nil
169+
default:
170+
return nil, fmt.Errorf("unexpected error")
165171
}
166-
return nil, fmt.Errorf("unexpected error")
167172
},
168173
allowed: true,
169174
},
@@ -172,12 +177,14 @@ func Test_Admit(t *testing.T) {
172177
oldUser: defaultUser.DeepCopy(),
173178
requestUserName: requesterUserName,
174179
resolverRulesFor: func(s string) ([]rbacv1.PolicyRule, error) {
175-
if s == requesterUserName {
180+
switch s {
181+
case requesterUserName:
176182
return getPods, nil
177-
} else if s == defaultUserName {
183+
case defaultUserName:
178184
return starPods, nil
185+
default:
186+
return nil, fmt.Errorf("unexpected error")
179187
}
180-
return nil, fmt.Errorf("unexpected error")
181188
},
182189
allowed: false,
183190
},
@@ -187,15 +194,54 @@ func Test_Admit(t *testing.T) {
187194
newUser: defaultUser.DeepCopy(),
188195
requestUserName: requesterUserName,
189196
resolverRulesFor: func(s string) ([]rbacv1.PolicyRule, error) {
190-
if s == requesterUserName {
197+
switch s {
198+
case requesterUserName:
191199
return getPods, nil
192-
} else if s == defaultUserName {
200+
case defaultUserName:
193201
return starPods, nil
202+
default:
203+
return nil, fmt.Errorf("unexpected error")
194204
}
195-
return nil, fmt.Errorf("unexpected error")
196205
},
197206
allowed: false,
198207
},
208+
{
209+
name: "changing the username not allowed",
210+
oldUser: defaultUser.DeepCopy(),
211+
newUser: &v3.User{
212+
ObjectMeta: metav1.ObjectMeta{
213+
Name: defaultUserName,
214+
},
215+
Username: "new-username",
216+
},
217+
requestUserName: requesterUserName,
218+
allowed: false,
219+
},
220+
{
221+
name: "adding a new username allowed",
222+
oldUser: &v3.User{
223+
ObjectMeta: metav1.ObjectMeta{
224+
Name: defaultUserName,
225+
},
226+
},
227+
newUser: defaultUser.DeepCopy(),
228+
requestUserName: requesterUserName,
229+
resolverRulesFor: func(string) ([]rbacv1.PolicyRule, error) {
230+
return getPods, nil
231+
},
232+
allowed: true,
233+
},
234+
{
235+
name: "removing username not allowed",
236+
oldUser: defaultUser.DeepCopy(),
237+
newUser: &v3.User{
238+
ObjectMeta: metav1.ObjectMeta{
239+
Name: defaultUserName,
240+
},
241+
},
242+
requestUserName: requesterUserName,
243+
allowed: false,
244+
},
199245
}
200246
for _, tt := range tests {
201247
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)