-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add webhook for CNV SSP resources (#527)
* Add SSP webhook * Add tests * Add more tests
- Loading branch information
1 parent
73d7db8
commit 69b6581
Showing
5 changed files
with
279 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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 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 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 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,70 @@ | ||
package validatingwebhook | ||
|
||
import ( | ||
"context" | ||
"html" | ||
"io" | ||
"net/http" | ||
|
||
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" | ||
|
||
userv1 "github.com/openshift/api/user/v1" | ||
"github.com/pkg/errors" | ||
admissionv1 "k8s.io/api/admission/v1" | ||
"k8s.io/apimachinery/pkg/types" | ||
runtimeClient "sigs.k8s.io/controller-runtime/pkg/client" | ||
) | ||
|
||
// The SSP resources is a CNV specific resource | ||
type SSPRequestValidator struct { | ||
Client runtimeClient.Client | ||
} | ||
|
||
func (v SSPRequestValidator) HandleValidate(w http.ResponseWriter, r *http.Request) { | ||
var respBody []byte | ||
body, err := io.ReadAll(r.Body) | ||
defer func() { | ||
if err := r.Body.Close(); err != nil { | ||
log.Error(err, "unable to close the body") | ||
} | ||
}() | ||
if err != nil { | ||
log.Error(err, "unable to read the body of the request") | ||
w.WriteHeader(http.StatusInternalServerError) | ||
respBody = []byte("unable to read the body of the request") | ||
} else { | ||
// validate the request | ||
respBody = v.validate(r.Context(), body) | ||
w.WriteHeader(http.StatusOK) | ||
} | ||
if _, err := io.WriteString(w, string(respBody)); err != nil { | ||
log.Error(err, "unable to write response") | ||
} | ||
} | ||
|
||
func (v SSPRequestValidator) validate(ctx context.Context, body []byte) []byte { | ||
log.Info("incoming request", "body", string(body)) | ||
admReview := admissionv1.AdmissionReview{} | ||
if _, _, err := deserializer.Decode(body, nil, &admReview); err != nil { | ||
// sanitize the body | ||
escapedBody := html.EscapeString(string(body)) | ||
log.Error(err, "unable to deserialize the admission review object", "body", escapedBody) | ||
return denyAdmissionRequest(admReview, errors.Wrapf(err, "unable to deserialize the admission review object - body: %v", escapedBody)) | ||
} | ||
//check if the requesting user is a sandbox user | ||
requestingUser := &userv1.User{} | ||
err := v.Client.Get(ctx, types.NamespacedName{ | ||
Name: admReview.Request.UserInfo.Username, | ||
}, requestingUser) | ||
|
||
if err != nil { | ||
log.Error(err, "unable to find the user requesting creation of the SSP resource", "username", admReview.Request.UserInfo.Username) | ||
return denyAdmissionRequest(admReview, errors.New("unable to find the user requesting the creation of the SSP resource")) | ||
} | ||
if requestingUser.GetLabels()[toolchainv1alpha1.ProviderLabelKey] == toolchainv1alpha1.ProviderLabelValue { | ||
log.Info("sandbox user is trying to create a SSP", "AdmissionReview", admReview) | ||
return denyAdmissionRequest(admReview, errors.New("this is a Dev Sandbox enforced restriction. you are trying to create a SSP resource, which is not allowed")) | ||
} | ||
// at this point, it is clear the user isn't a sandbox user, allow request | ||
return allowAdmissionRequest(admReview) | ||
} |
180 changes: 180 additions & 0 deletions
180
pkg/webhook/validatingwebhook/validate_ssp_request_test.go
This file contains 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,180 @@ | ||
package validatingwebhook | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"text/template" | ||
|
||
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" | ||
"github.com/codeready-toolchain/member-operator/pkg/webhook/validatingwebhook/test" | ||
|
||
userv1 "github.com/openshift/api/user/v1" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes/scheme" | ||
"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||
) | ||
|
||
func TestHandleValidateSSPAdmissionRequestBlocked(t *testing.T) { | ||
v := newSSPRequestValidator(t, "johnsmith", true) | ||
// given | ||
ts := httptest.NewServer(http.HandlerFunc(v.HandleValidate)) | ||
defer ts.Close() | ||
|
||
// when | ||
resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(newCreateSSPAdmissionRequest(t, SSPAdmReviewTmplParams{"CREATE", "johnsmith"}))) | ||
|
||
// then | ||
assert.NoError(t, err) | ||
body, err := io.ReadAll(resp.Body) | ||
defer func() { | ||
require.NoError(t, resp.Body.Close()) | ||
}() | ||
assert.NoError(t, err) | ||
test.VerifyRequestBlocked(t, body, "this is a Dev Sandbox enforced restriction. you are trying to create a SSP resource, which is not allowed", "b6ae2ab4-782b-11ee-b962-0242ac120002") | ||
} | ||
|
||
func TestValidateSSPAdmissionRequest(t *testing.T) { | ||
t.Run("sandbox user trying to create a SSP resource is denied", func(t *testing.T) { | ||
// given | ||
v := newSSPRequestValidator(t, "johnsmith", true) | ||
req := newCreateSSPAdmissionRequest(t, SSPAdmReviewTmplParams{"CREATE", "johnsmith"}) | ||
|
||
// when | ||
response := v.validate(context.TODO(), req) | ||
|
||
// then | ||
test.VerifyRequestBlocked(t, response, "this is a Dev Sandbox enforced restriction. you are trying to create a SSP resource, which is not allowed", "b6ae2ab4-782b-11ee-b962-0242ac120002") | ||
}) | ||
|
||
t.Run("sandbox user trying to update a SSP resource is denied", func(t *testing.T) { | ||
// given | ||
v := newSSPRequestValidator(t, "johnsmith", true) | ||
req := newCreateSSPAdmissionRequest(t, SSPAdmReviewTmplParams{"CREATE", "johnsmith"}) | ||
|
||
// when | ||
response := v.validate(context.TODO(), req) | ||
|
||
// then | ||
test.VerifyRequestBlocked(t, response, "this is a Dev Sandbox enforced restriction. you are trying to create a SSP resource, which is not allowed", "b6ae2ab4-782b-11ee-b962-0242ac120002") | ||
}) | ||
|
||
t.Run("non-sandbox user trying to create a SSP resource is allowed", func(t *testing.T) { | ||
// given | ||
v := newSSPRequestValidator(t, "other", false) | ||
req := newCreateSSPAdmissionRequest(t, SSPAdmReviewTmplParams{"CREATE", "other"}) | ||
|
||
// when | ||
response := v.validate(context.TODO(), req) | ||
|
||
// then | ||
test.VerifyRequestAllowed(t, response, "b6ae2ab4-782b-11ee-b962-0242ac120002") | ||
}) | ||
|
||
t.Run("non-sandbox user trying to update a SSP resource is allowed", func(t *testing.T) { | ||
// given | ||
v := newSSPRequestValidator(t, "other", false) | ||
req := newCreateSSPAdmissionRequest(t, SSPAdmReviewTmplParams{"UPDATE", "other"}) | ||
|
||
// when | ||
response := v.validate(context.TODO(), req) | ||
|
||
// then | ||
test.VerifyRequestAllowed(t, response, "b6ae2ab4-782b-11ee-b962-0242ac120002") | ||
}) | ||
|
||
} | ||
|
||
func newSSPRequestValidator(t *testing.T, username string, isSandboxUser bool) *SSPRequestValidator { | ||
s := scheme.Scheme | ||
err := userv1.Install(s) | ||
require.NoError(t, err) | ||
testUser := &userv1.User{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: username, | ||
}, | ||
} | ||
|
||
if isSandboxUser { | ||
testUser.Labels = map[string]string{ | ||
toolchainv1alpha1.ProviderLabelKey: toolchainv1alpha1.ProviderLabelValue, | ||
} | ||
} | ||
cl := fake.NewClientBuilder().WithScheme(s).WithObjects(testUser).Build() | ||
return &SSPRequestValidator{ | ||
Client: cl, | ||
} | ||
|
||
} | ||
|
||
func newCreateSSPAdmissionRequest(t *testing.T, params SSPAdmReviewTmplParams) []byte { | ||
tmpl, err := template.New("admission request").Parse(createSSPJSONTmpl) | ||
require.NoError(t, err) | ||
req := &bytes.Buffer{} | ||
err = tmpl.Execute(req, params) | ||
require.NoError(t, err) | ||
return req.Bytes() | ||
} | ||
|
||
type SSPAdmReviewTmplParams struct { | ||
ReqType string | ||
Username string | ||
} | ||
|
||
var createSSPJSONTmpl = `{ | ||
"kind": "AdmissionReview", | ||
"apiVersion": "admission.k8s.io/v1", | ||
"request": { | ||
"uid": "b6ae2ab4-782b-11ee-b962-0242ac120002", | ||
"kind": { | ||
"group": "ssp.kubevirt.io", | ||
"version": "v1alpha1", | ||
"kind": "SSP" | ||
}, | ||
"resource": { | ||
"group": "ssp.kubevirt.io", | ||
"version": "v1alpha1", | ||
"resource": "ssps" | ||
}, | ||
"requestKind": { | ||
"group": "ssp.kubevirt.io", | ||
"version": "v1alpha1", | ||
"kind": "SSP" | ||
}, | ||
"requestResource": { | ||
"group": "ssp.kubevirt.io", | ||
"version": "v1alpha1", | ||
"resource": "ssps" | ||
}, | ||
"name": "test", | ||
"namespace": "{{.Username}}-dev", | ||
"operation": "{{.ReqType}}", | ||
"userInfo": { | ||
"username": "{{.Username}}", | ||
"groups": [ | ||
"system:authenticated" | ||
] | ||
}, | ||
"object": { | ||
"apiVersion": "ssp.kubevirt.io", | ||
"kind": "SSP", | ||
"metadata": { | ||
"name": "{{.Username}}", | ||
"namespace": "{{.Username}}-dev" | ||
} | ||
}, | ||
"oldObject": null, | ||
"dryRun": false, | ||
"options": { | ||
"kind": "CreateOptions", | ||
"apiVersion": "meta.k8s.io/v1", | ||
"fieldManager": "kubectl-client-side-apply", | ||
"fieldValidation": "Ignore" | ||
} | ||
} | ||
}` |