Skip to content

Commit 5aba5c9

Browse files
authored
[v0.8] Introduce username validation (#1023) (#1024)
* 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 70acbc2 commit 5aba5c9

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
@@ -500,6 +500,12 @@ When a user is updated or deleted, a check occurs to ensure that the user making
500500

501501
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.
502502

503+
#### Invalid Fields - Update
504+
505+
Users can update the following fields if they had not been set. But after getting initial values, the fields cannot be changed:
506+
507+
- UserName
508+
503509
## UserAttribute
504510

505511
### 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
@@ -14,6 +14,7 @@ import (
1414
apierrors "k8s.io/apimachinery/pkg/api/errors"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/runtime/schema"
17+
"k8s.io/apimachinery/pkg/util/validation/field"
1718
"k8s.io/apiserver/pkg/authentication/user"
1819
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
1920
"k8s.io/kubernetes/pkg/registry/rbac/validation"
@@ -72,6 +73,19 @@ func (v *Validator) Admitters() []admission.Admitter {
7273
}
7374

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

8999
// Need the UserAttribute to find the groups
90-
userAttribute, err := a.userAttributeCache.Get(userObj.Name)
100+
userAttribute, err := a.userAttributeCache.Get(oldUser.Name)
91101
if err != nil && !apierrors.IsNotFound(err) {
92-
return nil, fmt.Errorf("failed to get UserAttribute for %s: %w", userObj.Name, err)
102+
return nil, fmt.Errorf("failed to get UserAttribute for %s: %w", oldUser.Name, err)
93103
}
94104

95105
userInfo := &user.DefaultInfo{
96-
Name: userObj.Name,
106+
Name: oldUser.Name,
97107
Groups: getGroupsFromUserAttribute(userAttribute),
98108
}
99109

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

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

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)