Skip to content
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

feat: make SpaceBindingRequest.Spec.MasterUserRecord immutable #470

Merged
merged 8 commits into from
Sep 11, 2023
Merged
10 changes: 7 additions & 3 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
"os/signal"
"syscall"

"github.com/codeready-toolchain/member-operator/pkg/apis"
"github.com/codeready-toolchain/member-operator/pkg/cert"
"github.com/codeready-toolchain/member-operator/pkg/klog"
"github.com/codeready-toolchain/member-operator/pkg/webhook/mutatingwebhook"
"github.com/codeready-toolchain/member-operator/pkg/webhook/validatingwebhook"

userv1 "github.com/openshift/api/user/v1"
"go.uber.org/zap/zapcore"
"k8s.io/apimachinery/pkg/runtime"
klogv1 "k8s.io/klog"
Expand Down Expand Up @@ -77,9 +77,9 @@ func main() {
setupLog.Error(err, "getting config failed")
os.Exit(1)
}
err = userv1.Install(runtimeScheme)
err = apis.AddToScheme(runtimeScheme)
if err != nil {
setupLog.Error(err, "adding user to scheme failed")
setupLog.Error(err, "adding apis to scheme failed")
os.Exit(1)
}
cl, err := client.New(cfg, client.Options{
Expand All @@ -95,11 +95,15 @@ func main() {
checlusterValidator := &validatingwebhook.CheClusterRequestValidator{
Client: cl,
}
spacebindingrequestValidator := &validatingwebhook.SpaceBindingRequestValidator{
Client: cl,
}
mux := http.NewServeMux()

mux.HandleFunc("/mutate-users-pods", mutatingwebhook.HandleMutate)
mux.HandleFunc("/validate-users-rolebindings", rolebindingValidator.HandleValidate)
mux.HandleFunc("/validate-users-checlusters", checlusterValidator.HandleValidate)
mux.HandleFunc("/validate-spacebindingrequests", spacebindingrequestValidator.HandleValidate)

webhookServer := &http.Server{ //nolint:gosec //TODO: configure ReadHeaderTimeout (gosec G112)
Addr: ":8443",
Expand Down
32 changes: 32 additions & 0 deletions deploy/webhook/member-operator-webhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ objects:
- get
- list
- watch
- apiGroups:
- "toolchain.dev.openshift.com"
resources:
- "spacebindingrequests"
verbs:
- get
- list
- watch
- apiVersion: v1
kind: ServiceAccount
metadata:
Expand Down Expand Up @@ -185,6 +193,30 @@ objects:
namespaceSelector:
matchLabels:
toolchain.dev.openshift.com/provider: codeready-toolchain
- name: users.spacebindingrequests.webhook.sandbox
admissionReviewVersions:
- v1
clientConfig:
caBundle: ${CA_BUNDLE}
service:
name: member-operator-webhook
namespace: ${NAMESPACE}
path: "/validate-spacebindingrequests"
port: 443
matchPolicy: Equivalent
rules:
- operations: ["CREATE", "UPDATE"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also need to cover PATCH, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember adding PATCH initially but was failing, since apparently there is not PATCH operations 🤷‍♂️ :https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules

operations lists one or more operations to match. Can be "CREATE", "UPDATE", "DELETE", "CONNECT", or "*" to match all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, OK. My bad - I automatically expected that Patch would be one of the operations as well :-)
Thanks for the link and sorry for confusion 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I expected the same and I'm still a bit confused of why are those the "operations". What is connect for example 😕

apiGroups: ["toolchain.dev.openshift.com"]
apiVersions: ["v1alpha1"]
resources: ["spacebindingrequests"]
scope: "Namespaced"
sideEffects: None
timeoutSeconds: 5
reinvocationPolicy: Never
failurePolicy: Ignore
namespaceSelector:
matchLabels:
toolchain.dev.openshift.com/provider: codeready-toolchain
parameters:
- name: NAMESPACE
value: 'toolchain-member-operator'
Expand Down
6 changes: 3 additions & 3 deletions make/go.mk
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ vendor:

.PHONY: generate-assets
generate-assets: go-bindata
@echo "generating users pods mutating webhook template data..."
@rm ./pkg/webhook/deploy/userspodswebhook/template_assets.go 2>/dev/null || true
@$(GO_BINDATA) -pkg userspodswebhook -o ./pkg/webhook/deploy/userspodswebhook/template_assets.go -nocompress -prefix deploy/webhook deploy/webhook
@echo "generating webhooks template data..."
@rm ./pkg/webhook/deploy/templates/template_assets.go 2>/dev/null || true
@$(GO_BINDATA) -pkg templates -o ./pkg/webhook/deploy/templates/template_assets.go -nocompress -prefix deploy/webhook deploy/webhook
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't webhook (or in plural form webhooks), or webhook_assest be more appropriate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed it to webhooks plz check : 9ddde44

@echo "generating autoscaler buffer template data..."
@rm ./pkg/autoscaler/template_assets.go 2>/dev/null || true
@$(GO_BINDATA) -pkg autoscaler -o ./pkg/autoscaler/template_assets.go -nocompress -prefix deploy/autoscaler deploy/autoscaler
Expand Down
5 changes: 3 additions & 2 deletions pkg/webhook/deploy/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package deploy

import (
"encoding/base64"

"github.com/codeready-toolchain/member-operator/pkg/cert"
"github.com/codeready-toolchain/member-operator/pkg/webhook/deploy/userspodswebhook"
"github.com/codeready-toolchain/member-operator/pkg/webhook/deploy/templates"
applycl "github.com/codeready-toolchain/toolchain-common/pkg/client"
"github.com/codeready-toolchain/toolchain-common/pkg/template"

Expand Down Expand Up @@ -45,7 +46,7 @@ func Webhook(cl runtimeclient.Client, s *runtime.Scheme, namespace, image string
}

func getTemplateObjects(s *runtime.Scheme, namespace, image string, caBundle []byte) ([]runtimeclient.Object, error) {
deployment, err := userspodswebhook.Asset("member-operator-webhook.yaml")
deployment, err := templates.Asset("member-operator-webhook.yaml")
if err != nil {
return nil, err
}
Expand Down
19 changes: 19 additions & 0 deletions pkg/webhook/validatingwebhook/test/objectmocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package test

var IncorrectRequestObjectJSON = []byte(`{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "a68769e5-d817-4617-bec5-90efa2bad6f8",
"name": "busybox1",
"namespace": "johnsmith-dev",
"object": {
"kind": "asbasbf",
"apiVersion": "v1",
"metadata": {
"name": "busybox1",
"namespace": "johnsmith-dev"
}
}
}
}`)
32 changes: 32 additions & 0 deletions pkg/webhook/validatingwebhook/test/verify_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package test

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
admissionv1 "k8s.io/api/admission/v1"
)

func VerifyRequestBlocked(t *testing.T, response []byte, msg string, UID string) {
reviewResponse := toReviewResponse(t, response)
assert.False(t, reviewResponse.Allowed)
assert.NotEmpty(t, reviewResponse.Result)
assert.Contains(t, reviewResponse.Result.Message, msg)
assert.Equal(t, UID, string(reviewResponse.UID))
}

func VerifyRequestAllowed(t *testing.T, response []byte, UID string) {
reviewResponse := toReviewResponse(t, response)
assert.True(t, reviewResponse.Allowed)
assert.Empty(t, reviewResponse.Result)
assert.Equal(t, UID, string(reviewResponse.UID))
}

func toReviewResponse(t *testing.T, content []byte) *admissionv1.AdmissionResponse {
r := admissionv1.AdmissionReview{}
err := json.Unmarshal(content, &r)
require.NoError(t, err)
return r.Response
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"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"
Expand All @@ -31,7 +31,7 @@ func TestHandleValidateCheClusterAdmissionRequest(t *testing.T) {
response := v.validate(req)

// then
verifyRequestDenied(t, response, "this is a Dev Sandbox enforced restriction. you are trying to create a CheCluster resource, which is not allowed", "f0b30997-3ac0-49f2-baf4-6eafd123564c")
test.VerifyRequestBlocked(t, response, "this is a Dev Sandbox enforced restriction. you are trying to create a CheCluster resource, which is not allowed", "f0b30997-3ac0-49f2-baf4-6eafd123564c")
})

t.Run("crtadmin user trying to create a CheCluster resource is allowed", func(t *testing.T) {
Expand All @@ -42,19 +42,11 @@ func TestHandleValidateCheClusterAdmissionRequest(t *testing.T) {
response := v.validate(req)

// then
verifyRequestAllowed(t, response, "f0b30997-3ac0-49f2-baf4-6eafd123564c")
test.VerifyRequestAllowed(t, response, "f0b30997-3ac0-49f2-baf4-6eafd123564c")
})

}

func verifyRequestDenied(t *testing.T, response []byte, msg string, UID string) {
reviewResponse := toReviewResponse(t, response)
assert.False(t, reviewResponse.Allowed)
assert.NotEmpty(t, reviewResponse.Result)
assert.Contains(t, reviewResponse.Result.Message, msg)
assert.Equal(t, UID, string(reviewResponse.UID))
}

func newCheClusterValidator(t *testing.T) *CheClusterRequestValidator {
s := scheme.Scheme
err := userv1.Install(s)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@ package validatingwebhook

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

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"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestHandleValidateRolebBndingAdmissionRequestBlocked(t *testing.T) {
func TestHandleValidateRolebBindingAdmissionRequestBlocked(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the extra b in Roleb expected?
it's the same also in other names of the test functions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While trying to fix a typo I've introduced a new one 🤦‍♂️ . Thanks it should be fixed in 9ddde44

v := newRoleBindingRequestValidator(t, "johnsmith", true)
// given
ts := httptest.NewServer(http.HandlerFunc(v.HandleValidate))
Expand All @@ -35,76 +34,76 @@ func TestHandleValidateRolebBndingAdmissionRequestBlocked(t *testing.T) {
require.NoError(t, resp.Body.Close())
}()
assert.NoError(t, err)
verifyRequestBlocked(t, body, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad6f6")
test.VerifyRequestBlocked(t, body, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad6f6")
}

func TestValidateRolebBndingAdmissionRequest(t *testing.T) {
func TestValidateRolebBindingAdmissionRequest(t *testing.T) {
t.Run("sandbox user trying to create rolebinding for all serviceaccounts is denied", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "johnsmith", true)
// when
response := v.validate(sandboxUserForAllServiceAccountsJSON)
// then
verifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad6f6")
test.VerifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad6f6")
})

t.Run("sandbox user trying to create rolebinding for all serviceaccounts: is denied", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "johnsmith", true)
// when
response := v.validate(sandboxUserForAllServiceAccountsJSON2)
// then
verifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad7g8")
test.VerifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad7g8")
})

t.Run("sandbox user trying to create rolebinding for all authenticated users is denied", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "johnsmith", true)
// when
response := v.validate(sandboxUserForAllUsersJSON)
// then
verifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad8k8")
test.VerifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad8k8")
})

t.Run("sandbox user trying to create rolebinding for all authenticated: is denied", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "johnsmith", true)
// when
response := v.validate(sandboxUserForAllUsersJSON2)
// then
verifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad9l9")
test.VerifyRequestBlocked(t, response, "please create a rolebinding for a specific user or service account to avoid this error", "a68769e5-d817-4617-bec5-90efa2bad9l9")
})
}

func TestValidateRolebBndingAdmissionRequestAllowed(t *testing.T) {
func TestValidateRolebBindingAdmissionRequestAllowed(t *testing.T) {

t.Run("SA or kubeadmin trying to create rolebinding is allowed", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "system:kubeadmin", false)
// when user is kubeadmin
response := v.validate(allowedUserJSON)

// then
verifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad6g7")
test.VerifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad6g7")
})

t.Run("non sandbox user trying to create rolebinding is allowed", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "nonsandbox", false)
// when
response := v.validate(nonSandboxUserJSON)
// then
verifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad6f7")
test.VerifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad6f7")
})

t.Run("unable to find the requesting user, allow request", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "random-user", true)
// when
response := v.validate(sandboxUserForAllServiceAccountsJSON)
// then
verifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad6f6")
test.VerifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad6f6")
})

t.Run("sandbox user creating a rolebinding for a specific user is allowed", func(t *testing.T) {
v := newRoleBindingRequestValidator(t, "laracroft", true)
//when
response := v.validate(allowedRbJSON)
//then
verifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad8g8")
test.VerifyRequestAllowed(t, response, "a68769e5-d817-4617-bec5-90efa2bad8g8")
})

}
Expand All @@ -118,40 +117,18 @@ func TestValidateRolebBndingAdmissionRequestFailsOnInvalidJson(t *testing.T) {
response := v.validate(rawJSON)

// then
verifyRequestBlocked(t, response, "cannot unmarshal string into Go value of type struct", "")
test.VerifyRequestBlocked(t, response, "cannot unmarshal string into Go value of type struct", "")
}

func TestValidateRolebBndingAdmissionRequestFailsOnInvalidObjectJson(t *testing.T) {
// given
v := &RoleBindingRequestValidator{}

// when
response := v.validate(incorrectRequestObjectJSON)
response := v.validate(test.IncorrectRequestObjectJSON)

// then
verifyRequestBlocked(t, response, "unable to unmarshal object or object is not a rolebinding", "a68769e5-d817-4617-bec5-90efa2bad6f8")
}

func verifyRequestBlocked(t *testing.T, response []byte, msg string, UID string) {
reviewResponse := toReviewResponse(t, response)
assert.False(t, reviewResponse.Allowed)
assert.NotEmpty(t, reviewResponse.Result)
assert.Contains(t, reviewResponse.Result.Message, msg)
assert.Equal(t, UID, string(reviewResponse.UID))
}

func verifyRequestAllowed(t *testing.T, response []byte, UID string) {
reviewResponse := toReviewResponse(t, response)
assert.True(t, reviewResponse.Allowed)
assert.Empty(t, reviewResponse.Result)
assert.Equal(t, UID, string(reviewResponse.UID))
}

func toReviewResponse(t *testing.T, content []byte) *admissionv1.AdmissionResponse {
r := admissionv1.AdmissionReview{}
err := json.Unmarshal(content, &r)
require.NoError(t, err)
return r.Response
test.VerifyRequestBlocked(t, response, "unable to unmarshal object or object is not a rolebinding", "a68769e5-d817-4617-bec5-90efa2bad6f8")
}

func newRoleBindingRequestValidator(t *testing.T, username string, sandboxUser bool) *RoleBindingRequestValidator {
Expand All @@ -173,23 +150,6 @@ func newRoleBindingRequestValidator(t *testing.T, username string, sandboxUser b
}
}

var incorrectRequestObjectJSON = []byte(`{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "a68769e5-d817-4617-bec5-90efa2bad6f8",
"name": "busybox1",
"namespace": "johnsmith-dev",
"object": {
"kind": "asbasbf",
"apiVersion": "v1",
"metadata": {
"name": "busybox1",
"namespace": "johnsmith-dev"
}
}
}
}`)
var sandboxUserForAllServiceAccountsJSON = []byte(`{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
Expand Down
Loading