Skip to content

Commit 5c54b7c

Browse files
Merge pull request #131 from boranx/OSD-3296/exclude-managed-resources
feat: add hiveownership webhook
2 parents 16615d3 + 4750fda commit 5c54b7c

File tree

4 files changed

+339
-0
lines changed

4 files changed

+339
-0
lines changed

build/selectorsyncset.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,40 @@ objects:
121121
scope: Cluster
122122
sideEffects: None
123123
timeoutSeconds: 2
124+
- apiVersion: admissionregistration.k8s.io/v1
125+
kind: ValidatingWebhookConfiguration
126+
metadata:
127+
annotations:
128+
service.beta.openshift.io/inject-cabundle: "true"
129+
creationTimestamp: null
130+
name: sre-hiveownership-validation
131+
webhooks:
132+
- admissionReviewVersions:
133+
- v1beta1
134+
clientConfig:
135+
service:
136+
name: validation-webhook
137+
namespace: openshift-validation-webhook
138+
path: /hiveownership-validation
139+
failurePolicy: Ignore
140+
matchPolicy: Equivalent
141+
name: hiveownership-validation.managed.openshift.io
142+
objectSelector:
143+
matchLabels:
144+
hive.openshift.io/managed: "true"
145+
rules:
146+
- apiGroups:
147+
- quota.openshift.io
148+
apiVersions:
149+
- '*'
150+
operations:
151+
- UPDATE
152+
- DELETE
153+
resources:
154+
- ClusterResourceQuota
155+
scope: Cluster
156+
sideEffects: None
157+
timeoutSeconds: 2
124158
- apiVersion: admissionregistration.k8s.io/v1
125159
kind: ValidatingWebhookConfiguration
126160
metadata:

pkg/webhooks/add_hiveownership.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/hiveownership"
5+
)
6+
7+
func init() {
8+
Register(hiveownership.WebhookName, func() Webhook { return hiveownership.NewWebhook() })
9+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package hiveownership
2+
3+
import (
4+
"sync"
5+
6+
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/utils"
7+
admissionregv1 "k8s.io/api/admissionregistration/v1"
8+
"k8s.io/api/apps/v1beta1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
12+
admissionctl "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
13+
)
14+
15+
// const
16+
const (
17+
WebhookName string = "hiveownership-validation"
18+
docString string = `Managed OpenShift customers may not edit certain managed resources. A managed resource has a "hive.openshift.io/managed": "true" label.`
19+
)
20+
21+
// HiveOwnershipWebhook denies requests
22+
// if it made by a customer to manage hive-labeled resources
23+
type HiveOwnershipWebhook struct {
24+
mu sync.Mutex
25+
s runtime.Scheme
26+
}
27+
28+
var (
29+
privilegedUsers = []string{"kube:admin", "system:admin", "system:serviceaccount:kube-system:generic-garbage-collector"}
30+
adminGroups = []string{"osd-sre-admins", "osd-sre-cluster-admins"}
31+
32+
log = logf.Log.WithName(WebhookName)
33+
34+
scope = admissionregv1.ClusterScope
35+
rules = []admissionregv1.RuleWithOperations{
36+
{
37+
Operations: []admissionregv1.OperationType{"UPDATE", "DELETE"},
38+
Rule: admissionregv1.Rule{
39+
APIGroups: []string{"quota.openshift.io"},
40+
APIVersions: []string{"*"},
41+
Resources: []string{"ClusterResourceQuota"},
42+
Scope: &scope,
43+
},
44+
},
45+
}
46+
)
47+
48+
// TimeoutSeconds implements Webhook interface
49+
func (s *HiveOwnershipWebhook) TimeoutSeconds() int32 { return 2 }
50+
51+
// MatchPolicy implements Webhook interface
52+
func (s *HiveOwnershipWebhook) MatchPolicy() admissionregv1.MatchPolicyType {
53+
return admissionregv1.Equivalent
54+
}
55+
56+
// Name implements Webhook interface
57+
func (s *HiveOwnershipWebhook) Name() string { return WebhookName }
58+
59+
// FailurePolicy implements Webhook interface
60+
func (s *HiveOwnershipWebhook) FailurePolicy() admissionregv1.FailurePolicyType {
61+
return admissionregv1.Ignore
62+
}
63+
64+
// Rules implements Webhook interface
65+
func (s *HiveOwnershipWebhook) Rules() []admissionregv1.RuleWithOperations { return rules }
66+
67+
// GetURI implements Webhook interface
68+
func (s *HiveOwnershipWebhook) GetURI() string { return "/" + WebhookName }
69+
70+
// SideEffects implements Webhook interface
71+
func (s *HiveOwnershipWebhook) SideEffects() admissionregv1.SideEffectClass {
72+
return admissionregv1.SideEffectClassNone
73+
}
74+
75+
// Validate is the incoming request even valid?
76+
func (s *HiveOwnershipWebhook) Validate(req admissionctl.Request) bool {
77+
valid := true
78+
valid = valid && (req.UserInfo.Username != "")
79+
80+
return valid
81+
}
82+
83+
// Doc documents the hook
84+
func (s *HiveOwnershipWebhook) Doc() string {
85+
return docString
86+
}
87+
88+
// ObjectSelector intercepts based on having the label
89+
// .metadata.labels["hive.openshift.io/managed"] == "true"
90+
func (s *HiveOwnershipWebhook) ObjectSelector() *metav1.LabelSelector {
91+
return &metav1.LabelSelector{
92+
MatchLabels: map[string]string{
93+
"hive.openshift.io/managed": "true",
94+
},
95+
}
96+
}
97+
98+
func (s *HiveOwnershipWebhook) authorized(request admissionctl.Request) admissionctl.Response {
99+
var ret admissionctl.Response
100+
101+
// Admin users
102+
if utils.SliceContains(request.AdmissionRequest.UserInfo.Username, privilegedUsers) {
103+
ret = admissionctl.Allowed("Admin users may edit managed resources")
104+
ret.UID = request.AdmissionRequest.UID
105+
return ret
106+
}
107+
// Users in admin groups
108+
for _, group := range request.AdmissionRequest.UserInfo.Groups {
109+
if utils.SliceContains(group, adminGroups) {
110+
ret = admissionctl.Allowed("Members of admin group may edit managed resources")
111+
ret.UID = request.AdmissionRequest.UID
112+
return ret
113+
}
114+
}
115+
116+
ret = admissionctl.Denied("Prevented from accessing Red Hat managed resources. This is in an effort to prevent harmful actions that may cause unintended consequences or affect the stability of the cluster. If you have any questions about this, please reach out to Red Hat support at https://access.redhat.com/support")
117+
ret.UID = request.AdmissionRequest.UID
118+
return ret
119+
}
120+
121+
// Authorized implements Webhook interface
122+
func (s *HiveOwnershipWebhook) Authorized(request admissionctl.Request) admissionctl.Response {
123+
return s.authorized(request)
124+
}
125+
126+
// NewWebhook creates a new webhook
127+
func NewWebhook() *HiveOwnershipWebhook {
128+
scheme := runtime.NewScheme()
129+
v1beta1.AddToScheme(scheme)
130+
131+
return &HiveOwnershipWebhook{
132+
s: *scheme,
133+
}
134+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package hiveownership
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/openshift/managed-cluster-validating-webhooks/pkg/testutils"
10+
11+
"k8s.io/api/admission/v1beta1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
14+
)
15+
16+
type hiveOwnershipTestSuites struct {
17+
testName string
18+
testID string
19+
username string
20+
userGroups []string
21+
oldObject *runtime.RawExtension
22+
operation v1beta1.Operation
23+
labels map[string]string
24+
shouldBeAllowed bool
25+
}
26+
27+
const testObjectRaw string = `{
28+
"metadata": {
29+
"name": "%s",
30+
"uid": "%s",
31+
"creationTimestamp": "2020-05-10T07:51:00Z",
32+
"labels": %s
33+
},
34+
"users": null
35+
}`
36+
37+
// labelsMapToString is a helper to turn a map into a JSON fragment to be
38+
// inserted into the testNamespaceRaw const. See createRawJSONString.
39+
func labelsMapToString(labels map[string]string) string {
40+
ret, _ := json.Marshal(labels)
41+
return string(ret)
42+
}
43+
44+
func createRawJSONString(name, uid string, labels map[string]string) string {
45+
return fmt.Sprintf(testObjectRaw, name, uid, labelsMapToString(labels))
46+
}
47+
func createOldObject(name, uid string, labels map[string]string) *runtime.RawExtension {
48+
return &runtime.RawExtension{
49+
Raw: []byte(createRawJSONString(name, uid, labels)),
50+
}
51+
}
52+
53+
func runTests(t *testing.T, tests []hiveOwnershipTestSuites) {
54+
gvk := metav1.GroupVersionKind{
55+
Group: "quota.openshift.io",
56+
Version: "v1",
57+
Kind: "ClusterResourceQuota",
58+
}
59+
gvr := metav1.GroupVersionResource{
60+
Group: "quota.openshift.io",
61+
Version: "v1",
62+
Resource: "clusterresourcequotas",
63+
}
64+
65+
for _, test := range tests {
66+
obj := createOldObject(test.testName, test.testID, test.labels)
67+
hook := NewWebhook()
68+
httprequest, err := testutils.CreateHTTPRequest(hook.GetURI(),
69+
test.testID,
70+
gvk, gvr, test.operation, test.username, test.userGroups, obj, test.oldObject)
71+
if err != nil {
72+
t.Fatalf("Expected no error, got %s", err.Error())
73+
}
74+
75+
response, err := testutils.SendHTTPRequest(httprequest, hook)
76+
if err != nil {
77+
t.Fatalf("Expected no error, got %s", err.Error())
78+
}
79+
if response.UID == "" {
80+
t.Fatalf("No tracking UID associated with the response: %+v", response)
81+
}
82+
83+
if response.Allowed != test.shouldBeAllowed {
84+
t.Fatalf("Mismatch: %s (groups=%s) %s %s. Test's expectation is that the user %s",
85+
test.username, test.userGroups,
86+
testutils.CanCanNot(response.Allowed), string(test.operation),
87+
testutils.CanCanNot(test.shouldBeAllowed))
88+
}
89+
}
90+
}
91+
92+
func TestThing(t *testing.T) {
93+
tests := []hiveOwnershipTestSuites{
94+
{
95+
testID: "kube-admin-test",
96+
username: "kube:admin",
97+
userGroups: []string{"kube:system", "system:authenticated", "system:authenticated:oauth"},
98+
operation: v1beta1.Create,
99+
shouldBeAllowed: true,
100+
},
101+
{
102+
testID: "sre-test",
103+
username: "sre-foo@redhat.com",
104+
userGroups: []string{adminGroups[0], "system:authenticated", "system:authenticated:oauth"},
105+
operation: v1beta1.Update,
106+
shouldBeAllowed: true,
107+
},
108+
{
109+
// dedicated-admin users. This should be blocked as making changes as CU on clusterresourcequota which are managed are prohibited.
110+
testID: "dedicated-admin-test",
111+
username: "bob@foo.com",
112+
userGroups: []string{"dedicated-admins", "system:authenticated", "system:authenticated:oauth"},
113+
operation: v1beta1.Update,
114+
labels: map[string]string{"hive.openshift.io/managed": "true"},
115+
shouldBeAllowed: false,
116+
},
117+
{
118+
// no special privileges, only an authenticated user. This should be blocked as making changes on clusterresourcequota which are managed are prohibited.
119+
testID: "unpriv-update-test",
120+
username: "unpriv-user",
121+
userGroups: []string{"system:authenticated", "system:authenticated:oauth"},
122+
operation: v1beta1.Update,
123+
labels: map[string]string{"hive.openshift.io/managed": "true"},
124+
shouldBeAllowed: false,
125+
},
126+
}
127+
runTests(t, tests)
128+
}
129+
130+
func TestBadRequests(t *testing.T) {
131+
t.Skip()
132+
}
133+
134+
func TestName(t *testing.T) {
135+
if NewWebhook().Name() == "" {
136+
t.Fatalf("Empty hook name")
137+
}
138+
}
139+
140+
func TestRules(t *testing.T) {
141+
if len(NewWebhook().Rules()) == 0 {
142+
t.Log("No rules for this webhook?")
143+
}
144+
}
145+
146+
func TestGetURI(t *testing.T) {
147+
if NewWebhook().GetURI()[0] != '/' {
148+
t.Fatalf("Hook URI does not begin with a /")
149+
}
150+
}
151+
152+
func TestObjectSelector(t *testing.T) {
153+
obj := &metav1.LabelSelector{
154+
MatchLabels: map[string]string{
155+
"hive.openshift.io/managed": "true",
156+
},
157+
}
158+
159+
if !reflect.DeepEqual(NewWebhook().ObjectSelector(), obj) {
160+
t.Fatalf("hive managed resources label name is not correct.")
161+
}
162+
}

0 commit comments

Comments
 (0)