Skip to content

Commit 88f6b48

Browse files
committed
added add_clusterrole.go add_clusterrole.go clusterrole_test.go
1 parent d46a27d commit 88f6b48

File tree

3 files changed

+426
-0
lines changed

3 files changed

+426
-0
lines changed

pkg/webhooks/add_clusterrole.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package webhooks
2+
3+
import (
4+
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/clusterrole"
5+
)
6+
7+
func init() {
8+
Register(clusterrole.WebhookName, func() Webhook { return clusterrole.NewWebhook() })
9+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package clusterrole
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"slices"
8+
"strings"
9+
10+
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/utils"
11+
admissionv1 "k8s.io/api/admission/v1"
12+
admissionregv1 "k8s.io/api/admissionregistration/v1"
13+
corev1 "k8s.io/api/core/v1"
14+
rbacv1 "k8s.io/api/rbac/v1"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/runtime"
17+
logf "sigs.k8s.io/controller-runtime/pkg/log"
18+
admissionctl "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
19+
)
20+
21+
const (
22+
WebhookName string = "clusterroles-validation"
23+
docString string = `Managed OpenShift Customers may not delete protected ClusterRoles including cluster-admin, view, edit, admin, specific system roles (system:admin, system:node, system:node-proxier, system:kube-scheduler, system:kube-controller-manager), and backplane-* roles`
24+
)
25+
26+
var (
27+
timeout int32 = 2
28+
log = logf.Log.WithName(WebhookName)
29+
scope = admissionregv1.ClusterScope
30+
rules = []admissionregv1.RuleWithOperations{
31+
{
32+
Operations: []admissionregv1.OperationType{"DELETE"},
33+
Rule: admissionregv1.Rule{
34+
APIGroups: []string{"rbac.authorization.k8s.io"},
35+
APIVersions: []string{"v1"},
36+
Resources: []string{"clusterroles"},
37+
Scope: &scope,
38+
},
39+
},
40+
}
41+
42+
// Protected ClusterRoles that should not be deleted
43+
protectedClusterRoles = []string{
44+
"cluster-admin",
45+
"view",
46+
"edit",
47+
"admin",
48+
"system:admin",
49+
"system:node",
50+
"system:node-proxier",
51+
"system:kube-scheduler",
52+
"system:kube-controller-manager",
53+
}
54+
55+
// Users allowed to delete protected ClusterRoles
56+
allowedUsers = []string{
57+
"backplane-cluster-admin",
58+
}
59+
60+
// Groups allowed to delete protected ClusterRoles
61+
allowedGroups = []string{
62+
"system:serviceaccounts:openshift-backplane-srep",
63+
}
64+
)
65+
66+
type ClusterRoleWebHook struct {
67+
s runtime.Scheme
68+
}
69+
70+
// NewWebhook creates the new webhook
71+
func NewWebhook() *ClusterRoleWebHook {
72+
scheme := runtime.NewScheme()
73+
err := admissionv1.AddToScheme(scheme)
74+
if err != nil {
75+
log.Error(err, "Fail adding admissionsv1 scheme to ClusterRoleWebHook")
76+
os.Exit(1)
77+
}
78+
err = corev1.AddToScheme(scheme)
79+
if err != nil {
80+
log.Error(err, "Fail adding corev1 scheme to ClusterRoleWebHook")
81+
os.Exit(1)
82+
}
83+
84+
return &ClusterRoleWebHook{
85+
s: *scheme,
86+
}
87+
}
88+
89+
// Authorized implements Webhook interface
90+
func (s *ClusterRoleWebHook) Authorized(request admissionctl.Request) admissionctl.Response {
91+
return s.authorized(request)
92+
}
93+
94+
func (s *ClusterRoleWebHook) authorized(request admissionctl.Request) admissionctl.Response {
95+
var ret admissionctl.Response
96+
97+
if request.AdmissionRequest.UserInfo.Username == "system:unauthenticated" {
98+
log.Info("system:unauthenticated made a webhook request. Check RBAC rules", "request", request.AdmissionRequest)
99+
ret = admissionctl.Denied("Unauthenticated")
100+
ret.UID = request.AdmissionRequest.UID
101+
return ret
102+
}
103+
104+
if strings.HasPrefix(request.AdmissionRequest.UserInfo.Username, "system:") && request.AdmissionRequest.UserInfo.Username != "system:admin" {
105+
ret = admissionctl.Allowed("authenticated system: users are allowed")
106+
ret.UID = request.AdmissionRequest.UID
107+
return ret
108+
}
109+
110+
if strings.HasPrefix(request.AdmissionRequest.UserInfo.Username, "kube:") {
111+
ret = admissionctl.Allowed("kube: users are allowed")
112+
ret.UID = request.AdmissionRequest.UID
113+
return ret
114+
}
115+
116+
clusterRole, err := s.renderClusterRole(request)
117+
if err != nil {
118+
log.Error(err, "Couldn't render a ClusterRole from the incoming request")
119+
return admissionctl.Errored(http.StatusBadRequest, err)
120+
}
121+
122+
log.Info(fmt.Sprintf("Found clusterrole: %v", clusterRole.Name))
123+
124+
if isProtectedClusterRole(clusterRole) && !isAllowedUserGroup(request) {
125+
switch request.Operation {
126+
case admissionv1.Delete:
127+
log.Info(fmt.Sprintf("Deleting operation detected on ClusterRole: %v", clusterRole.Name))
128+
129+
ret = admissionctl.Denied(fmt.Sprintf("Deleting ClusterRole %v is not allowed", clusterRole.Name))
130+
ret.UID = request.AdmissionRequest.UID
131+
return ret
132+
}
133+
}
134+
135+
ret = admissionctl.Allowed("Request is allowed")
136+
ret.UID = request.AdmissionRequest.UID
137+
return ret
138+
}
139+
140+
// renderClusterRole renders the ClusterRole object from the request
141+
func (s *ClusterRoleWebHook) renderClusterRole(request admissionctl.Request) (*rbacv1.ClusterRole, error) {
142+
decoder := admissionctl.NewDecoder(&s.s)
143+
clusterRole := &rbacv1.ClusterRole{}
144+
145+
var err error
146+
if len(request.OldObject.Raw) > 0 {
147+
err = decoder.DecodeRaw(request.OldObject, clusterRole)
148+
}
149+
if err != nil {
150+
return nil, err
151+
}
152+
153+
return clusterRole, nil
154+
}
155+
156+
// isAllowedUserGroup checks if the user or group is allowed to perform the action
157+
func isAllowedUserGroup(request admissionctl.Request) bool {
158+
if slices.Contains(allowedUsers, request.UserInfo.Username) {
159+
return true
160+
}
161+
for _, group := range allowedGroups {
162+
if slices.Contains(request.UserInfo.Groups, group) {
163+
return true
164+
}
165+
}
166+
return false
167+
}
168+
169+
// isProtectedClusterRole returns true if the ClusterRole is in the protected list or matches protected patterns
170+
func isProtectedClusterRole(clusterRole *rbacv1.ClusterRole) bool {
171+
// Check if it's in the explicit protected list (includes specific system roles)
172+
if slices.Contains(protectedClusterRoles, clusterRole.Name) {
173+
return true
174+
}
175+
176+
// Check if it matches backplane pattern
177+
if strings.HasPrefix(clusterRole.Name, "backplane-") {
178+
return true
179+
}
180+
181+
return false
182+
}
183+
184+
// GetURI implements Webhook interface
185+
func (s *ClusterRoleWebHook) GetURI() string {
186+
return "/" + WebhookName
187+
}
188+
189+
// Validate implements Webhook interface
190+
func (s *ClusterRoleWebHook) Validate(request admissionctl.Request) bool {
191+
valid := true
192+
valid = valid && (request.UserInfo.Username != "")
193+
valid = valid && (request.Kind.Kind == "ClusterRole")
194+
195+
return valid
196+
}
197+
198+
// Name implements Webhook interface
199+
func (s *ClusterRoleWebHook) Name() string {
200+
return WebhookName
201+
}
202+
203+
// FailurePolicy implements Webhook interface
204+
func (s *ClusterRoleWebHook) FailurePolicy() admissionregv1.FailurePolicyType {
205+
return admissionregv1.Ignore
206+
}
207+
208+
// MatchPolicy implements Webhook interface
209+
func (s *ClusterRoleWebHook) MatchPolicy() admissionregv1.MatchPolicyType {
210+
return admissionregv1.Equivalent
211+
}
212+
213+
// Rules implements Webhook interface
214+
func (s *ClusterRoleWebHook) Rules() []admissionregv1.RuleWithOperations {
215+
return rules
216+
}
217+
218+
// ObjectSelector implements Webhook interface
219+
func (s *ClusterRoleWebHook) ObjectSelector() *metav1.LabelSelector {
220+
return nil
221+
}
222+
223+
// SideEffects implements Webhook interface
224+
func (s *ClusterRoleWebHook) SideEffects() admissionregv1.SideEffectClass {
225+
return admissionregv1.SideEffectClassNone
226+
}
227+
228+
// TimeoutSeconds implements Webhook interface
229+
func (s *ClusterRoleWebHook) TimeoutSeconds() int32 {
230+
return timeout
231+
}
232+
233+
// Doc implements Webhook interface
234+
func (s *ClusterRoleWebHook) Doc() string {
235+
return docString
236+
}
237+
238+
// SyncSetLabelSelector returns the label selector to use in the SyncSet.
239+
// Return utils.DefaultLabelSelector() to stick with the default
240+
func (s *ClusterRoleWebHook) SyncSetLabelSelector() metav1.LabelSelector {
241+
return utils.DefaultLabelSelector()
242+
}
243+
244+
func (s *ClusterRoleWebHook) ClassicEnabled() bool { return true }
245+
246+
func (s *ClusterRoleWebHook) HypershiftEnabled() bool { return true }

0 commit comments

Comments
 (0)