-
Notifications
You must be signed in to change notification settings - Fork 92
[SREP-1045] Develop Clusterrole webhook #379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package webhooks | ||
|
||
import ( | ||
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/clusterrole" | ||
) | ||
|
||
func init() { | ||
Register(clusterrole.WebhookName, func() Webhook { return clusterrole.NewWebhook() }) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
package clusterrole | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"slices" | ||
"strings" | ||
|
||
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/utils" | ||
admissionv1 "k8s.io/api/admission/v1" | ||
admissionregv1 "k8s.io/api/admissionregistration/v1" | ||
corev1 "k8s.io/api/core/v1" | ||
rbacv1 "k8s.io/api/rbac/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
logf "sigs.k8s.io/controller-runtime/pkg/log" | ||
admissionctl "sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
) | ||
|
||
const ( | ||
WebhookName string = "clusterroles-validation" | ||
backplanePrefix string = "backplane-" | ||
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` | ||
) | ||
|
||
var ( | ||
timeout int32 = 2 | ||
log = logf.Log.WithName(WebhookName) | ||
scope = admissionregv1.ClusterScope | ||
rules = []admissionregv1.RuleWithOperations{ | ||
{ | ||
Operations: []admissionregv1.OperationType{"DELETE"}, | ||
Rule: admissionregv1.Rule{ | ||
APIGroups: []string{"rbac.authorization.k8s.io"}, | ||
APIVersions: []string{"v1"}, | ||
Resources: []string{"clusterroles"}, | ||
Scope: &scope, | ||
}, | ||
}, | ||
} | ||
|
||
// Protected ClusterRoles that should not be deleted | ||
protectedClusterRoles = []string{ | ||
"cluster-admin", | ||
"view", | ||
"edit", | ||
"admin", | ||
"system:admin", | ||
"system:node", | ||
"system:kube-scheduler", | ||
"system:kube-controller-manager", | ||
} | ||
|
||
// Users allowed to delete protected ClusterRoles | ||
allowedUsers = []string{ | ||
"backplane-cluster-admin", | ||
} | ||
|
||
// Groups allowed to delete protected ClusterRoles | ||
allowedGroups = []string{ | ||
"system:serviceaccounts:openshift-backplane-srep", | ||
} | ||
) | ||
|
||
type ClusterRoleWebHook struct { | ||
s runtime.Scheme | ||
} | ||
|
||
// NewWebhook creates the new webhook | ||
func NewWebhook() *ClusterRoleWebHook { | ||
scheme := runtime.NewScheme() | ||
err := admissionv1.AddToScheme(scheme) | ||
if err != nil { | ||
log.Error(err, "Fail adding admissionsv1 scheme to ClusterRoleWebHook") | ||
os.Exit(1) | ||
} | ||
err = corev1.AddToScheme(scheme) | ||
if err != nil { | ||
log.Error(err, "Fail adding corev1 scheme to ClusterRoleWebHook") | ||
os.Exit(1) | ||
} | ||
|
||
return &ClusterRoleWebHook{ | ||
s: *scheme, | ||
} | ||
} | ||
|
||
// Authorized implements Webhook interface | ||
func (s *ClusterRoleWebHook) Authorized(request admissionctl.Request) admissionctl.Response { | ||
return s.authorized(request) | ||
} | ||
|
||
func (s *ClusterRoleWebHook) authorized(request admissionctl.Request) admissionctl.Response { | ||
var ret admissionctl.Response | ||
|
||
if request.AdmissionRequest.UserInfo.Username == "system:unauthenticated" { | ||
log.Info("system:unauthenticated made a webhook request. Check RBAC rules", "request", request.AdmissionRequest) | ||
ret = admissionctl.Denied("Unauthenticated") | ||
ret.UID = request.AdmissionRequest.UID | ||
return ret | ||
} | ||
|
||
if strings.HasPrefix(request.AdmissionRequest.UserInfo.Username, "system:") && request.AdmissionRequest.UserInfo.Username != "system:admin" { | ||
ret = admissionctl.Allowed("authenticated system: users are allowed") | ||
ret.UID = request.AdmissionRequest.UID | ||
return ret | ||
} | ||
|
||
if strings.HasPrefix(request.AdmissionRequest.UserInfo.Username, "kube:") { | ||
ret = admissionctl.Allowed("kube: users are allowed") | ||
ret.UID = request.AdmissionRequest.UID | ||
return ret | ||
} | ||
|
||
clusterRole, err := s.renderClusterRole(request) | ||
if err != nil { | ||
log.Error(err, "Couldn't render a ClusterRole from the incoming request") | ||
return admissionctl.Errored(http.StatusBadRequest, err) | ||
} | ||
|
||
log.Info(fmt.Sprintf("Found clusterrole: %v", clusterRole.Name)) | ||
|
||
if isProtectedClusterRole(clusterRole) && !isAllowedUserGroup(request) { | ||
switch request.Operation { | ||
case admissionv1.Delete: | ||
log.Info(fmt.Sprintf("Deleting operation detected on ClusterRole: %v", clusterRole.Name)) | ||
|
||
ret = admissionctl.Denied(fmt.Sprintf("Deleting ClusterRole %v is not allowed", clusterRole.Name)) | ||
ret.UID = request.AdmissionRequest.UID | ||
return ret | ||
} | ||
} | ||
|
||
ret = admissionctl.Allowed("Request is allowed") | ||
ret.UID = request.AdmissionRequest.UID | ||
return ret | ||
} | ||
|
||
// renderClusterRole renders the ClusterRole object from the request | ||
func (s *ClusterRoleWebHook) renderClusterRole(request admissionctl.Request) (*rbacv1.ClusterRole, error) { | ||
decoder := admissionctl.NewDecoder(&s.s) | ||
clusterRole := &rbacv1.ClusterRole{} | ||
|
||
var err error | ||
if len(request.OldObject.Raw) > 0 { | ||
err = decoder.DecodeRaw(request.OldObject, clusterRole) | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return clusterRole, nil | ||
} | ||
|
||
// isAllowedUserGroup checks if the user or group is allowed to perform the action | ||
func isAllowedUserGroup(request admissionctl.Request) bool { | ||
if slices.Contains(allowedUsers, request.UserInfo.Username) { | ||
return true | ||
} | ||
for _, group := range allowedGroups { | ||
if slices.Contains(request.UserInfo.Groups, group) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// isProtectedClusterRole returns true if the ClusterRole is in the protected list or matches protected patterns | ||
func isProtectedClusterRole(clusterRole *rbacv1.ClusterRole) bool { | ||
// Check if it's in the explicit protected list (includes specific system roles) | ||
if slices.Contains(protectedClusterRoles, clusterRole.Name) { | ||
return true | ||
} | ||
// Check if it matches backplane pattern | ||
if strings.HasPrefix(clusterRole.Name, backplanePrefix) { | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
// GetURI implements Webhook interface | ||
func (s *ClusterRoleWebHook) GetURI() string { | ||
return "/" + WebhookName | ||
} | ||
|
||
// Validate implements Webhook interface | ||
func (s *ClusterRoleWebHook) Validate(request admissionctl.Request) bool { | ||
valid := true | ||
valid = valid && (request.UserInfo.Username != "") | ||
valid = valid && (request.Kind.Kind == "ClusterRole") | ||
|
||
return valid | ||
} | ||
|
||
// Name implements Webhook interface | ||
func (s *ClusterRoleWebHook) Name() string { | ||
return WebhookName | ||
} | ||
|
||
// FailurePolicy implements Webhook interface | ||
func (s *ClusterRoleWebHook) FailurePolicy() admissionregv1.FailurePolicyType { | ||
return admissionregv1.Ignore | ||
} | ||
|
||
// MatchPolicy implements Webhook interface | ||
func (s *ClusterRoleWebHook) MatchPolicy() admissionregv1.MatchPolicyType { | ||
return admissionregv1.Equivalent | ||
} | ||
|
||
// Rules implements Webhook interface | ||
func (s *ClusterRoleWebHook) Rules() []admissionregv1.RuleWithOperations { | ||
return rules | ||
} | ||
|
||
// ObjectSelector implements Webhook interface | ||
func (s *ClusterRoleWebHook) ObjectSelector() *metav1.LabelSelector { | ||
return nil | ||
} | ||
|
||
// SideEffects implements Webhook interface | ||
func (s *ClusterRoleWebHook) SideEffects() admissionregv1.SideEffectClass { | ||
return admissionregv1.SideEffectClassNone | ||
} | ||
|
||
// TimeoutSeconds implements Webhook interface | ||
func (s *ClusterRoleWebHook) TimeoutSeconds() int32 { | ||
return timeout | ||
} | ||
|
||
// Doc implements Webhook interface | ||
func (s *ClusterRoleWebHook) Doc() string { | ||
return docString | ||
} | ||
|
||
// SyncSetLabelSelector returns the label selector to use in the SyncSet. | ||
// Return utils.DefaultLabelSelector() to stick with the default | ||
func (s *ClusterRoleWebHook) SyncSetLabelSelector() metav1.LabelSelector { | ||
return utils.DefaultLabelSelector() | ||
} | ||
|
||
func (s *ClusterRoleWebHook) ClassicEnabled() bool { return true } | ||
|
||
func (s *ClusterRoleWebHook) HypershiftEnabled() bool { return true } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a full list? it mentioned backplane-* in the description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The full list will contain any backplane related clusterroles + most openshift critical. Its not fiesible to include all
system:*
roles as they can change over time, and we still want customers being able to create and remove their own clusterroles. The idea is being able to get access to the cluster if some clusterroles have been deleted.