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

🌱 adding SubjectAccessReview.authorization.k8s.io/v1 webhook support #1535

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions examples/subjectaccessreview/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"os"

_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"sigs.k8s.io/controller-runtime/pkg/webhook/authorization"
)

func init() {
log.SetLogger(zap.New())
}

func main() {
entryLog := log.Log.WithName("entrypoint")

// Setup a Manager
entryLog.Info("setting up manager")
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
if err != nil {
entryLog.Error(err, "unable to set up overall controller manager")
os.Exit(1)
}

// Setup webhooks
entryLog.Info("setting up webhook server")
hookServer := mgr.GetWebhookServer()

entryLog.Info("registering webhooks to the webhook server")
hookServer.Register("/validate-v1-subjectaccessreview", &authorization.Webhook{Handler: &authorizer{}})

entryLog.Info("starting manager")
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
entryLog.Error(err, "unable to run manager")
os.Exit(1)
}
}
38 changes: 38 additions & 0 deletions examples/subjectaccessreview/subjectaccessreview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"

"sigs.k8s.io/controller-runtime/pkg/webhook/authorization"
)

// authorizer validates subjectaccessreviews
type authorizer struct {
}

// authorizer admits a request by the token.
func (a *authorizer) Handle(ctx context.Context, req authorization.Request) authorization.Response {
if req.Spec.User == "system:anonymous" {
return authorization.Denied("anonymous users are not allowed")
}
if req.Spec.User == "foo" {
return authorization.NoOpinion("I don't care if foo is authorized or not")
}
return authorization.Allowed()
}
43 changes: 15 additions & 28 deletions pkg/webhook/admission/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err = errors.New("request body is empty")
wh.log.Error(err, "bad request")
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, nil, reviewResponse)
return
}

defer r.Body.Close()
if body, err = ioutil.ReadAll(r.Body); err != nil {
wh.log.Error(err, "unable to read the body from the incoming request")
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, nil, reviewResponse)
return
}

Expand All @@ -72,7 +72,7 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err = fmt.Errorf("contentType=%s, expected application/json", contentType)
wh.log.Error(err, "unable to process a request with an unknown content type", "content type", contentType)
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, nil, reviewResponse)
return
}

Expand All @@ -91,51 +91,38 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
wh.log.Error(err, "unable to decode the request")
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, actualAdmRevGVK, reviewResponse)
return
}
wh.log.V(1).Info("received request", "UID", req.UID, "kind", req.Kind, "resource", req.Resource)

reviewResponse = wh.Handle(ctx, req)
wh.writeResponseTyped(w, reviewResponse, actualAdmRevGVK)
wh.writeResponse(w, actualAdmRevGVK, reviewResponse)
}

// writeResponse writes response to w generically, i.e. without encoding GVK information.
func (wh *Webhook) writeResponse(w io.Writer, response Response) {
wh.writeAdmissionResponse(w, v1.AdmissionReview{Response: &response.AdmissionResponse})
}

// writeResponseTyped writes response to w with GVK set to admRevGVK, which is necessary
// if multiple AdmissionReview versions are permitted by the webhook.
func (wh *Webhook) writeResponseTyped(w io.Writer, response Response, admRevGVK *schema.GroupVersionKind) {
func (wh *Webhook) writeResponse(w io.Writer, gvk *schema.GroupVersionKind, response Response) {
ar := v1.AdmissionReview{
Response: &response.AdmissionResponse,
}
// Default to a v1 AdmissionReview, otherwise the API server may not recognize the request
// if multiple AdmissionReview versions are permitted by the webhook config.
// TODO(estroz): this should be configurable since older API servers won't know about v1.
if admRevGVK == nil || *admRevGVK == (schema.GroupVersionKind{}) {
if gvk == nil || *gvk == (schema.GroupVersionKind{}) {
ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview"))
} else {
ar.SetGroupVersionKind(*admRevGVK)
ar.SetGroupVersionKind(*gvk)
}
wh.writeAdmissionResponse(w, ar)
}

// writeAdmissionResponse writes ar to w.
func (wh *Webhook) writeAdmissionResponse(w io.Writer, ar v1.AdmissionReview) {
if err := json.NewEncoder(w).Encode(ar); err != nil {
wh.log.Error(err, "unable to encode the response")
wh.writeResponse(w, Errored(http.StatusInternalServerError, err))
} else {
res := ar.Response
if log := wh.log; log.V(1).Enabled() {
if res.Result != nil {
log = log.WithValues("code", res.Result.Code, "reason", res.Result.Reason)
}
log.V(1).Info("wrote response", "UID", res.UID, "allowed", res.Allowed)
}
wh.writeResponse(w, gvk, Errored(http.StatusInternalServerError, err))
}

logger := wh.log.V(1)
if result := ar.Response.Result; result != nil {
logger = logger.WithValues("code", result.Code, "reason", result.Reason)
}
logger.V(1).Info("wrote response", "UID", ar.Response.UID, "allowed", ar.Response.Allowed)
}

// unversionedAdmissionReview is used to decode both v1 and v1beta1 AdmissionReview types.
Expand Down
19 changes: 11 additions & 8 deletions pkg/webhook/admission/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ var _ = Describe("Admission Webhooks", func() {
It("should return bad-request when given an empty body", func() {
req := &http.Request{Body: nil}

expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
`
expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
`, gvkJSONv1)

webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})
Expand All @@ -68,9 +69,10 @@ var _ = Describe("Admission Webhooks", func() {
Body: nopCloser{Reader: bytes.NewBuffer(nil)},
}

expected :=
`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"contentType=application/foo, expected application/json","code":400}}}
`
expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":false,"status":{"metadata":{},`+
`"message":"contentType=application/foo, expected application/json","code":400}}}
`, gvkJSONv1)

webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})
Expand All @@ -81,9 +83,10 @@ var _ = Describe("Admission Webhooks", func() {
Body: nopCloser{Reader: bytes.NewBufferString("{")},
}

expected :=
`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"couldn't get version/kind; json parse error: unexpected end of JSON input","code":400}}}
`
expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":false,"status":{"metadata":{},`+
`"message":"couldn't get version/kind; json parse error: unexpected end of JSON input","code":400}}}
`, gvkJSONv1)

webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})
Expand Down
44 changes: 19 additions & 25 deletions pkg/webhook/authentication/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err = errors.New("request body is empty")
wh.log.Error(err, "bad request")
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, nil, reviewResponse)
return
}

defer r.Body.Close()
if body, err = ioutil.ReadAll(r.Body); err != nil {
wh.log.Error(err, "unable to read the body from the incoming request")
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, nil, reviewResponse)
return
}

Expand All @@ -72,7 +72,7 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err = fmt.Errorf("contentType=%s, expected application/json", contentType)
wh.log.Error(err, "unable to process a request with an unknown content type", "content type", contentType)
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, nil, reviewResponse)
return
}

Expand All @@ -92,7 +92,7 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
wh.log.Error(err, "unable to decode the request")
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, actualTokRevGVK, reviewResponse)
return
}
wh.log.V(1).Info("received request", "UID", req.UID, "kind", req.Kind)
Expand All @@ -101,44 +101,38 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err = errors.New("token is empty")
wh.log.Error(err, "bad request")
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
wh.writeResponse(w, actualTokRevGVK, reviewResponse)
return
}

reviewResponse = wh.Handle(ctx, req)
wh.writeResponseTyped(w, reviewResponse, actualTokRevGVK)
wh.writeResponse(w, actualTokRevGVK, reviewResponse)
}

// writeResponse writes response to w generically, i.e. without encoding GVK information.
func (wh *Webhook) writeResponse(w io.Writer, response Response) {
wh.writeTokenResponse(w, response.TokenReview)
}

// writeResponseTyped writes response to w with GVK set to tokRevGVK, which is necessary
// if multiple TokenReview versions are permitted by the webhook.
func (wh *Webhook) writeResponseTyped(w io.Writer, response Response, tokRevGVK *schema.GroupVersionKind) {
func (wh *Webhook) writeResponse(w io.Writer, gvk *schema.GroupVersionKind, response Response) {
ar := response.TokenReview

// Default to a v1 TokenReview, otherwise the API server may not recognize the request
// if multiple TokenReview versions are permitted by the webhook config.
if tokRevGVK == nil || *tokRevGVK == (schema.GroupVersionKind{}) {
if gvk == nil || *gvk == (schema.GroupVersionKind{}) {
ar.SetGroupVersionKind(authenticationv1.SchemeGroupVersion.WithKind("TokenReview"))
} else {
ar.SetGroupVersionKind(*tokRevGVK)
ar.SetGroupVersionKind(*gvk)
}
wh.writeTokenResponse(w, ar)
}

// writeTokenResponse writes ar to w.
func (wh *Webhook) writeTokenResponse(w io.Writer, ar authenticationv1.TokenReview) {
if err := json.NewEncoder(w).Encode(ar); err != nil {
wh.log.Error(err, "unable to encode the response")
wh.writeResponse(w, Errored(err))
}
res := ar
if log := wh.log; log.V(1).Enabled() {
log.V(1).Info("wrote response", "UID", res.UID, "authenticated", res.Status.Authenticated)
wh.writeResponse(w, gvk, Errored(err))
}

wh.log.
V(1).
WithValues(
"uid", ar.UID,
"authenticated", ar.Status.Authenticated,
"error", ar.Status.Error,
).
Info("wrote response")
}

// unversionedTokenReview is used to decode both v1 and v1beta1 TokenReview types.
Expand Down
23 changes: 15 additions & 8 deletions pkg/webhook/authentication/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ var _ = Describe("Authentication Webhooks", func() {
It("should return bad-request when given an empty body", func() {
req := &http.Request{Body: nil}

expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request body is empty"}}
`
expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},`+
`"error":"request body is empty"}}
`, gvkJSONv1)

webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})
Expand All @@ -68,8 +70,10 @@ var _ = Describe("Authentication Webhooks", func() {
Body: nopCloser{Reader: bytes.NewBuffer(nil)},
}

expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"contentType=application/foo, expected application/json"}}
`
expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},`+
`"error":"contentType=application/foo, expected application/json"}}
`, gvkJSONv1)

webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})
Expand All @@ -81,8 +85,10 @@ var _ = Describe("Authentication Webhooks", func() {
Body: nopCloser{Reader: bytes.NewBufferString("{")},
}

expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"couldn't get version/kind; json parse error: unexpected end of JSON input"}}
`
expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},`+
`"error":"couldn't get version/kind; json parse error: unexpected end of JSON input"}}
`, gvkJSONv1)

webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})
Expand All @@ -94,8 +100,9 @@ var _ = Describe("Authentication Webhooks", func() {
Body: nopCloser{Reader: bytes.NewBufferString(`{"spec":{"token":""}}`)},
}

expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"token is empty"}}
`
expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"token is empty"}}
`, gvkJSONv1)

webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})
Expand Down
Loading