-
Notifications
You must be signed in to change notification settings - Fork 1.2k
🌱 adding TokenReview.auth.k8s.io/v1 webhook support #1440
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} | ||
} |
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{}) | ||
} |
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) |
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") |
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 { | ||
err = errors.New("request body is empty") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the godocs of net/http.Request:
So this doesn't need to be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is another carryover from the admission package. https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/webhook/admission/http.go#L53-L67 That being said since we're not actually checking the HTTP Method, I believe this is what this block is meant to confirm that this isn't a GET, but I could have also understood the goals of it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think I should remove this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've switched around the way this is handled, the first check |
||
wh.log.Error(err, "bad request") | ||
reviewResponse = Errored(err) | ||
wh.writeResponse(w, 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) | ||
return | ||
} | ||
|
||
// verify the content type is accurate | ||
contentType := r.Header.Get("Content-Type") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this check needed? Deserialization will fail if its not json or not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was carried over from the admission package. https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/webhook/admission/http.go#L69-L77 I'd guess this is to give a bit more visibility into what actually went wrong in the request. Should I remove it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I guess that isn't what happens, the decoder doesn't seem to check what the actual headers are. If you don't have this check and the body is valid the request could be successful or could fail other places. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you get better errors this way too -- if you try to deserialize proto as json, for instance, you just end up with a really confusing error, whereas if you explicitly check |
||
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{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is maybe more dangerous that it might seem? What if a new field is added to v1beta1 that's actually different than v1 (they're not guarnateed to be the same, just round-trippable). We could end up making a decision on bad data, right? I suppose for new named fields this is the same as operating on old types against a new apiserver. If Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If so I can add a note to the comment above. A lot of this was carried over from the Admissions webhook, even this comment tbh. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now I've added a comment, mentioning that |
||
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 | ||
} | ||
|
||
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{} |
Uh oh!
There was an error while loading. Please reload this page.