Skip to content

Commit 80a0001

Browse files
authored
Add new User validation webhook (#786) (#787)
* Add new User validation * Fix linter errors
1 parent c4778ee commit 80a0001

File tree

9 files changed

+533
-0
lines changed

9 files changed

+533
-0
lines changed

docs.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,16 @@ When a Token is updated, the following checks take place:
403403

404404
- If set, `lastUsedAt` must be a valid date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
405405

406+
## User
407+
408+
### Validation Checks
409+
410+
#### Update and Delete
411+
412+
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.
413+
414+
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.
415+
406416
## UserAttribute
407417

408418
### Validation Checks

pkg/codegen/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func main() {
5454
v3.Feature{},
5555
v3.Setting{},
5656
v3.User{},
57+
v3.UserAttribute{},
5758
},
5859
},
5960
"provisioning.cattle.io": {
@@ -90,6 +91,7 @@ func main() {
9091
&v3.NodeDriver{},
9192
&v3.Project{},
9293
&v3.Setting{},
94+
&v3.User{},
9395
},
9496
},
9597
"provisioning.cattle.io": {

pkg/generated/controllers/management.cattle.io/v3/interface.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/generated/controllers/management.cattle.io/v3/userattribute.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/generated/objects/management.cattle.io/v3/objects.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,56 @@ func SettingFromRequest(request *admissionv1.AdmissionRequest) (*v3.Setting, err
643643

644644
return object, nil
645645
}
646+
647+
// UserOldAndNewFromRequest gets the old and new User objects, respectively, from the webhook request.
648+
// If the request is a Delete operation, then the new object is the zero value for User.
649+
// Similarly, if the request is a Create operation, then the old object is the zero value for User.
650+
func UserOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v3.User, *v3.User, error) {
651+
if request == nil {
652+
return nil, nil, fmt.Errorf("nil request")
653+
}
654+
655+
object := &v3.User{}
656+
oldObject := &v3.User{}
657+
658+
if request.Operation != admissionv1.Delete {
659+
err := json.Unmarshal(request.Object.Raw, object)
660+
if err != nil {
661+
return nil, nil, fmt.Errorf("failed to unmarshal request object: %w", err)
662+
}
663+
}
664+
665+
if request.Operation == admissionv1.Create {
666+
return oldObject, object, nil
667+
}
668+
669+
err := json.Unmarshal(request.OldObject.Raw, oldObject)
670+
if err != nil {
671+
return nil, nil, fmt.Errorf("failed to unmarshal request oldObject: %w", err)
672+
}
673+
674+
return oldObject, object, nil
675+
}
676+
677+
// UserFromRequest returns a User object from the webhook request.
678+
// If the operation is a Delete operation, then the old object is returned.
679+
// Otherwise, the new object is returned.
680+
func UserFromRequest(request *admissionv1.AdmissionRequest) (*v3.User, error) {
681+
if request == nil {
682+
return nil, fmt.Errorf("nil request")
683+
}
684+
685+
object := &v3.User{}
686+
raw := request.Object.Raw
687+
688+
if request.Operation == admissionv1.Delete {
689+
raw = request.OldObject.Raw
690+
}
691+
692+
err := json.Unmarshal(raw, object)
693+
if err != nil {
694+
return nil, fmt.Errorf("failed to unmarshal request object: %w", err)
695+
}
696+
697+
return object, nil
698+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## Validation Checks
2+
3+
### Update and Delete
4+
5+
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.
6+
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.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package users
2+
3+
import (
4+
"fmt"
5+
6+
v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
7+
"github.com/rancher/webhook/pkg/admission"
8+
"github.com/rancher/webhook/pkg/auth"
9+
controllerv3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3"
10+
objectsv3 "github.com/rancher/webhook/pkg/generated/objects/management.cattle.io/v3"
11+
admissionv1 "k8s.io/api/admission/v1"
12+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
13+
apierrors "k8s.io/apimachinery/pkg/api/errors"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/runtime/schema"
16+
"k8s.io/apiserver/pkg/authentication/user"
17+
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
18+
"k8s.io/kubernetes/pkg/registry/rbac/validation"
19+
)
20+
21+
var (
22+
gvr = schema.GroupVersionResource{
23+
Group: "management.cattle.io",
24+
Version: "v3",
25+
Resource: "users",
26+
}
27+
manageUsersVerb = "manage-users"
28+
)
29+
30+
type admitter struct {
31+
resolver validation.AuthorizationRuleResolver
32+
sar authorizationv1.SubjectAccessReviewInterface
33+
userAttributeCache controllerv3.UserAttributeCache
34+
}
35+
36+
// Validator validates tokens.
37+
type Validator struct {
38+
admitter admitter
39+
}
40+
41+
// NewValidator returns a new Validator instance.
42+
func NewValidator(userAttributeCache controllerv3.UserAttributeCache, sar authorizationv1.SubjectAccessReviewInterface, defaultResolver validation.AuthorizationRuleResolver) *Validator {
43+
return &Validator{
44+
admitter: admitter{
45+
resolver: defaultResolver,
46+
userAttributeCache: userAttributeCache,
47+
sar: sar,
48+
},
49+
}
50+
}
51+
52+
// GVR returns the GroupVersionResource.
53+
func (v *Validator) GVR() schema.GroupVersionResource {
54+
return gvr
55+
}
56+
57+
// Operations returns list of operations handled by the validator.
58+
func (v *Validator) Operations() []admissionregistrationv1.OperationType {
59+
return []admissionregistrationv1.OperationType{admissionregistrationv1.Update, admissionregistrationv1.Delete}
60+
}
61+
62+
func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.WebhookClientConfig) []admissionregistrationv1.ValidatingWebhook {
63+
return []admissionregistrationv1.ValidatingWebhook{
64+
*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.ClusterScope, v.Operations()),
65+
}
66+
}
67+
68+
// Admitters returns the admitter objects.
69+
func (v *Validator) Admitters() []admission.Admitter {
70+
return []admission.Admitter{&v.admitter}
71+
}
72+
73+
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
74+
// Check if requester has manage-user verb
75+
hasManageUsers, err := auth.RequestUserHasVerb(request, gvr, a.sar, manageUsersVerb, "", "")
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to check if requester has manage-users verb: %w", err)
78+
}
79+
80+
if hasManageUsers {
81+
return &admissionv1.AdmissionResponse{Allowed: true}, nil
82+
}
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+
}
87+
88+
// Need the UserAttribute to find the groups
89+
userAttribute, err := a.userAttributeCache.Get(userObj.Name)
90+
if err != nil && !apierrors.IsNotFound(err) {
91+
return nil, fmt.Errorf("failed to get UserAttribute for %s: %w", userObj.Name, err)
92+
}
93+
94+
userInfo := &user.DefaultInfo{
95+
Name: userObj.Name,
96+
Groups: getGroupsFromUserAttribute(userAttribute),
97+
}
98+
99+
// Get all rules for the user being modified
100+
rules, err := a.resolver.RulesFor(userInfo, "")
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to get rules for user %v: %w", userObj, err)
103+
}
104+
105+
// Ensure that rules of the user being modified aren't greater than the rules of the user making the request
106+
err = auth.ConfirmNoEscalation(request, rules, "", a.resolver)
107+
if err != nil {
108+
return &admissionv1.AdmissionResponse{
109+
Allowed: false,
110+
Result: &metav1.Status{
111+
Status: metav1.StatusFailure,
112+
Reason: "ConfirmNoEscalationError",
113+
Message: fmt.Sprintf("request is attempting to modify user with more permissions than requester %v", err),
114+
},
115+
}, nil
116+
}
117+
return &admissionv1.AdmissionResponse{Allowed: true}, nil
118+
}
119+
120+
// getGroupsFromUserAttributes gets the list of group principals from a UserAttribute.
121+
//
122+
// Warning: UserAttributes are only updated when a user logs in, so this may not have the up to date Group Principals.
123+
func getGroupsFromUserAttribute(userAttribute *v3.UserAttribute) []string {
124+
result := []string{}
125+
if userAttribute == nil {
126+
return result
127+
}
128+
for _, principals := range userAttribute.GroupPrincipals {
129+
for _, principal := range principals.Items {
130+
result = append(result, principal.Name)
131+
}
132+
}
133+
return result
134+
}

0 commit comments

Comments
 (0)