Skip to content

Commit

Permalink
adding TokenReview.auth.k8s.io/v1 webhook support
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Hein <me@chrishein.com>
  • Loading branch information
christopherhein committed Apr 6, 2021
1 parent 11dfabf commit 53d0278
Show file tree
Hide file tree
Showing 10 changed files with 1,048 additions and 0 deletions.
58 changes: 58 additions & 0 deletions examples/tokenreview/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/authentication"
)

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-tokenreview", &authentication.Webhook{Handler: &authenticator{}})

entryLog.Info("starting manager")
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
entryLog.Error(err, "unable to run manager")
os.Exit(1)
}
}
37 changes: 37 additions & 0 deletions examples/tokenreview/tokenreview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
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"

v1 "k8s.io/api/authentication/v1"

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

// authenticator validates tokenreviews
type authenticator struct {
}

// authenticator admits a request by the token.
func (a *authenticator) Handle(ctx context.Context, req authentication.Request) authentication.Response {
if req.Spec.Token == "invalid" {
return authentication.Unauthenticated("invalid is an invalid token", v1.UserInfo{})
}
return authentication.Authenticated("", v1.UserInfo{})
}
40 changes: 40 additions & 0 deletions pkg/webhook/authentication/authentication_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
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 authentication

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

func TestAuthenticationWebhook(t *testing.T) {
RegisterFailHandler(Fail)
suiteName := "Authentication Webhook Suite"
RunSpecsWithDefaultAndCustomReporters(t, suiteName, []Reporter{printer.NewlineReporter{}, printer.NewProwReporter(suiteName)})
}

var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

close(done)
}, 60)
29 changes: 29 additions & 0 deletions pkg/webhook/authentication/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
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 authentication provides implementation for authentication webhook and
methods to implement authentication webhook handlers.
See examples/tokenreview/ for an example of authentication webhooks.
*/
package authentication

import (
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
)

var log = logf.RuntimeLog.WithName("authentication")
151 changes: 151 additions & 0 deletions pkg/webhook/authentication/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
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 authentication

import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"

authenticationv1 "k8s.io/api/authentication/v1"
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)

var authenticationScheme = runtime.NewScheme()
var authenticationCodecs = serializer.NewCodecFactory(authenticationScheme)

func init() {
utilruntime.Must(authenticationv1.AddToScheme(authenticationScheme))
utilruntime.Must(authenticationv1beta1.AddToScheme(authenticationScheme))
}

var _ http.Handler = &Webhook{}

func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body []byte
var err error
ctx := r.Context()
if wh.WithContextFunc != nil {
ctx = wh.WithContextFunc(ctx, r)
}

var reviewResponse Response
if r.Body != nil {
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)
return
}
} else {
err = errors.New("request body is empty")
wh.log.Error(err, "bad request")
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
return
}

// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
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)
return
}

// Both v1 and v1beta1 TokenReview types are exactly the same, so the v1beta1 type can
// be decoded into the v1 type. The v1beta1 api is deprecated as of 1.19 and will be
// removed in authenticationv1.22. However the runtime codec's decoder guesses which type to
// decode into by type name if an Object's TypeMeta isn't set. By setting TypeMeta of an
// unregistered type to the v1 GVK, the decoder will coerce a v1beta1 TokenReview to authenticationv1.
// The actual TokenReview GVK will be used to write a typed response in case the
// webhook config permits multiple versions, otherwise this response will fail.
req := Request{}
ar := unversionedTokenReview{}
// avoid an extra copy
ar.TokenReview = &req.TokenReview
ar.SetGroupVersionKind(authenticationv1.SchemeGroupVersion.WithKind("TokenReview"))
_, actualTokRevGVK, err := authenticationCodecs.UniversalDeserializer().Decode(body, nil, &ar)
if err != nil {
wh.log.Error(err, "unable to decode the request")
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
return
}
wh.log.V(1).Info("received request", "UID", req.UID, "kind", req.Kind)

if req.Spec.Token == "" {
err = errors.New("token is empty")
wh.log.Error(err, "bad request")
reviewResponse = Errored(err)
wh.writeResponse(w, reviewResponse)
return
}

// TODO: add panic-recovery for Handle
reviewResponse = wh.Handle(ctx, req)
wh.writeResponseTyped(w, reviewResponse, actualTokRevGVK)
}

// 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) {
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{}) {
ar.SetGroupVersionKind(authenticationv1.SchemeGroupVersion.WithKind("TokenReview"))
} else {
ar.SetGroupVersionKind(*tokRevGVK)
}
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)
}
return
}

// unversionedTokenReview is used to decode both v1 and v1beta1 TokenReview types.
type unversionedTokenReview struct {
*authenticationv1.TokenReview
}

var _ runtime.Object = &unversionedTokenReview{}
Loading

0 comments on commit 53d0278

Please sign in to comment.