Skip to content

Commit 21222a6

Browse files
authored
[SREP-1045] Develop Clusterrole webhook (#379)
* added add_clusterrole.go add_clusterrole.go clusterrole_test.go * make sync and doc generate
1 parent d46a27d commit 21222a6

File tree

4 files changed

+454
-0
lines changed

4 files changed

+454
-0
lines changed

build/selectorsyncset.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,36 @@ objects:
274274
scope: Cluster
275275
sideEffects: None
276276
timeoutSeconds: 2
277+
- apiVersion: admissionregistration.k8s.io/v1
278+
kind: ValidatingWebhookConfiguration
279+
metadata:
280+
annotations:
281+
service.beta.openshift.io/inject-cabundle: "true"
282+
creationTimestamp: null
283+
name: sre-clusterroles-validation
284+
webhooks:
285+
- admissionReviewVersions:
286+
- v1
287+
clientConfig:
288+
service:
289+
name: validation-webhook
290+
namespace: openshift-validation-webhook
291+
path: /clusterroles-validation
292+
failurePolicy: Ignore
293+
matchPolicy: Equivalent
294+
name: clusterroles-validation.managed.openshift.io
295+
rules:
296+
- apiGroups:
297+
- rbac.authorization.k8s.io
298+
apiVersions:
299+
- v1
300+
operations:
301+
- DELETE
302+
resources:
303+
- clusterroles
304+
scope: Cluster
305+
sideEffects: None
306+
timeoutSeconds: 2
277307
- apiVersion: admissionregistration.k8s.io/v1
278308
kind: ValidatingWebhookConfiguration
279309
metadata:

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: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
backplanePrefix string = "backplane-"
24+
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`
25+
)
26+
27+
var (
28+
timeout int32 = 2
29+
log = logf.Log.WithName(WebhookName)
30+
scope = admissionregv1.ClusterScope
31+
rules = []admissionregv1.RuleWithOperations{
32+
{
33+
Operations: []admissionregv1.OperationType{"DELETE"},
34+
Rule: admissionregv1.Rule{
35+
APIGroups: []string{"rbac.authorization.k8s.io"},
36+
APIVersions: []string{"v1"},
37+
Resources: []string{"clusterroles"},
38+
Scope: &scope,
39+
},
40+
},
41+
}
42+
43+
// Protected ClusterRoles that should not be deleted
44+
protectedClusterRoles = []string{
45+
"cluster-admin",
46+
"view",
47+
"edit",
48+
"admin",
49+
"system:admin",
50+
"system:node",
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+
// Check if it matches backplane pattern
176+
if strings.HasPrefix(clusterRole.Name, backplanePrefix) {
177+
return true
178+
}
179+
return false
180+
}
181+
182+
// GetURI implements Webhook interface
183+
func (s *ClusterRoleWebHook) GetURI() string {
184+
return "/" + WebhookName
185+
}
186+
187+
// Validate implements Webhook interface
188+
func (s *ClusterRoleWebHook) Validate(request admissionctl.Request) bool {
189+
valid := true
190+
valid = valid && (request.UserInfo.Username != "")
191+
valid = valid && (request.Kind.Kind == "ClusterRole")
192+
193+
return valid
194+
}
195+
196+
// Name implements Webhook interface
197+
func (s *ClusterRoleWebHook) Name() string {
198+
return WebhookName
199+
}
200+
201+
// FailurePolicy implements Webhook interface
202+
func (s *ClusterRoleWebHook) FailurePolicy() admissionregv1.FailurePolicyType {
203+
return admissionregv1.Ignore
204+
}
205+
206+
// MatchPolicy implements Webhook interface
207+
func (s *ClusterRoleWebHook) MatchPolicy() admissionregv1.MatchPolicyType {
208+
return admissionregv1.Equivalent
209+
}
210+
211+
// Rules implements Webhook interface
212+
func (s *ClusterRoleWebHook) Rules() []admissionregv1.RuleWithOperations {
213+
return rules
214+
}
215+
216+
// ObjectSelector implements Webhook interface
217+
func (s *ClusterRoleWebHook) ObjectSelector() *metav1.LabelSelector {
218+
return nil
219+
}
220+
221+
// SideEffects implements Webhook interface
222+
func (s *ClusterRoleWebHook) SideEffects() admissionregv1.SideEffectClass {
223+
return admissionregv1.SideEffectClassNone
224+
}
225+
226+
// TimeoutSeconds implements Webhook interface
227+
func (s *ClusterRoleWebHook) TimeoutSeconds() int32 {
228+
return timeout
229+
}
230+
231+
// Doc implements Webhook interface
232+
func (s *ClusterRoleWebHook) Doc() string {
233+
return docString
234+
}
235+
236+
// SyncSetLabelSelector returns the label selector to use in the SyncSet.
237+
// Return utils.DefaultLabelSelector() to stick with the default
238+
func (s *ClusterRoleWebHook) SyncSetLabelSelector() metav1.LabelSelector {
239+
return utils.DefaultLabelSelector()
240+
}
241+
242+
func (s *ClusterRoleWebHook) ClassicEnabled() bool { return true }
243+
244+
func (s *ClusterRoleWebHook) HypershiftEnabled() bool { return true }

0 commit comments

Comments
 (0)