Fix update/patch and add webhook auth

This commit is contained in:
Darren Shepherd
2019-09-11 10:32:01 -07:00
parent 7f5d248680
commit ef235af49c
22 changed files with 1453 additions and 195 deletions

16
main.go
View File

@@ -23,18 +23,22 @@ func main() {
app.Version = version.FriendlyVersion()
app.Usage = ""
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "authentication",
Destination: &config.Authentication,
},
cli.StringFlag{
Name: "webhook-kubeconfig",
EnvVar: "WEBHOOK_KUBECONFIG",
Value: "webhook-kubeconfig.yaml",
Destination: &config.WebhookKubeconfig,
},
cli.StringFlag{
Name: "kubeconfig",
EnvVar: "KUBECONFIG",
Value: "",
Destination: &config.Kubeconfig,
},
cli.StringFlag{
Name: "namespace",
EnvVar: "NAMESPACE",
Value: "default",
Destination: &config.Namespace,
},
cli.StringFlag{
Name: "listen-address",
EnvVar: "LISTEN_ADDRESS",

View File

@@ -6,13 +6,17 @@ import (
"sync/atomic"
"time"
"github.com/rancher/naok/pkg/attributes"
schema2 "github.com/rancher/naok/pkg/resources/schema"
"github.com/rancher/naok/pkg/resources/schema/converter"
"github.com/rancher/norman/pkg/types"
apiextcontrollerv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1"
v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io/v1"
"github.com/sirupsen/logrus"
authorizationv1 "k8s.io/api/authorization/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/client-go/discovery"
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
apiv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
)
@@ -27,6 +31,7 @@ type handler struct {
schemas *schema2.Collection
client discovery.DiscoveryInterface
crd apiextcontrollerv1beta1.CustomResourceDefinitionClient
ssar authorizationv1client.SelfSubjectAccessReviewInterface
handler SchemasHandler
}
@@ -34,6 +39,7 @@ func Register(ctx context.Context,
discovery discovery.DiscoveryInterface,
crd apiextcontrollerv1beta1.CustomResourceDefinitionController,
apiService v1.APIServiceController,
ssar authorizationv1client.SelfSubjectAccessReviewInterface,
schemasHandler SchemasHandler,
schemas *schema2.Collection) (init func() error) {
@@ -42,6 +48,7 @@ func Register(ctx context.Context,
schemas: schemas,
handler: schemasHandler,
crd: crd,
ssar: ssar,
}
apiService.OnChange(ctx, "schema", h.OnChangeAPIService)
@@ -75,6 +82,24 @@ func (h *handler) queueRefresh() {
}()
}
func isListWatchable(schema *types.Schema) bool {
var (
canList bool
canWatch bool
)
for _, verb := range attributes.Verbs(schema) {
switch verb {
case "list":
canList = true
case "watch":
canWatch = true
}
}
return canList && canWatch
}
func (h *handler) refreshAll() error {
h.Lock()
defer h.Unlock()
@@ -89,7 +114,19 @@ func (h *handler) refreshAll() error {
return err
}
h.schemas.Reset(schemas)
filteredSchemas := map[string]*types.Schema{}
for id, schema := range schemas {
if isListWatchable(schema) {
if ok, err := h.allowed(schema); err != nil {
return err
} else if !ok {
continue
}
}
filteredSchemas[id] = schema
}
h.schemas.Reset(filteredSchemas)
if h.handler != nil {
return h.handler.OnSchemas(h.schemas)
}
@@ -97,6 +134,24 @@ func (h *handler) refreshAll() error {
return nil
}
func (h *handler) allowed(schema *types.Schema) (bool, error) {
gvr := attributes.GVR(schema)
ssar, err := h.ssar.Create(&authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Verb: "list",
Group: gvr.Group,
Version: gvr.Version,
Resource: gvr.Resource,
},
},
})
if err != nil {
return false, err
}
return ssar.Status.Allowed && !ssar.Status.Denied, nil
}
func (h *handler) needToSync() bool {
old := atomic.SwapInt32(&h.toSync, 0)
return old == 1

View File

@@ -2,33 +2,32 @@ package server
import (
"context"
"github.com/rancher/norman/pkg/auth"
"net/http"
"github.com/rancher/wrangler/pkg/generic"
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
"github.com/rancher/naok/pkg/clustercache"
"github.com/rancher/wrangler-api/pkg/generated/controllers/core"
"github.com/rancher/naok/pkg/accesscontrol"
"github.com/rancher/naok/pkg/client"
"github.com/rancher/naok/pkg/clustercache"
"github.com/rancher/naok/pkg/controllers/schema"
"github.com/rancher/naok/pkg/resources"
"github.com/rancher/naok/pkg/server/publicapi"
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io"
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io"
"github.com/rancher/wrangler-api/pkg/generated/controllers/core"
rbaccontroller "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac"
"github.com/rancher/wrangler/pkg/generic"
"github.com/rancher/wrangler/pkg/kubeconfig"
"github.com/rancher/wrangler/pkg/start"
"github.com/sirupsen/logrus"
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
)
type Config struct {
Kubeconfig string
Namespace string
ListenAddress string
WebhookKubeconfig string
Authentication bool
}
func Run(ctx context.Context, cfg Config) error {
@@ -37,6 +36,9 @@ func Run(ctx context.Context, cfg Config) error {
return err
}
restConfig.QPS = 100
restConfig.Burst = 100
rbac, err := rbaccontroller.NewFactoryFromConfig(restConfig)
if err != nil {
return err
@@ -80,6 +82,7 @@ func Run(ctx context.Context, cfg Config) error {
k8s.Discovery(),
crd.Apiextensions().V1beta1().CustomResourceDefinition(),
api.Apiregistration().V1().APIService(),
k8s.AuthorizationV1().SelfSubjectAccessReviews(),
ccache,
sf)
@@ -88,6 +91,14 @@ func Run(ctx context.Context, cfg Config) error {
return err
}
if cfg.Authentication {
authMiddleware, err := auth.NewWebhookMiddleware(cfg.WebhookKubeconfig)
if err != nil {
return err
}
handler = wrapHandler(handler, authMiddleware)
}
for _, controllers := range []controllers{api, crd, rbac} {
for gvk, controller := range controllers.Controllers() {
ccache.AddController(gvk, controller.Informer())
@@ -106,6 +117,12 @@ func Run(ctx context.Context, cfg Config) error {
return http.ListenAndServe(cfg.ListenAddress, handler)
}
func wrapHandler(handler http.Handler, middleware func(http.ResponseWriter, *http.Request, http.Handler)) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
middleware(resp, req, handler)
})
}
type controllers interface {
Controllers() map[schema2.GroupVersionKind]*generic.Controller
}

View File

@@ -195,6 +195,8 @@ func determineVerb(apiOp *types.APIRequest) Verb {
return Get
case http.MethodPost:
return Create
case http.MethodPatch:
return Update
case http.MethodPut:
return Update
case http.MethodDelete:

93
vendor/github.com/rancher/norman/pkg/auth/filter.go generated vendored Normal file
View File

@@ -0,0 +1,93 @@
package auth
import (
"net/http"
"strings"
v1 "k8s.io/api/authentication/v1"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
)
type Authenticator interface {
Authenticate(req *http.Request) (user.Info, bool, error)
}
func NewWebhookAuthenticator(kubeConfigFile string) (Authenticator, error) {
wh, err := webhook.New(kubeConfigFile, nil)
if err != nil {
return nil, err
}
return &webhookAuth{
auth: wh,
}, nil
}
func NewWebhookMiddleware(kubeConfigFile string) (func(http.ResponseWriter, *http.Request, http.Handler), error) {
auth, err := NewWebhookAuthenticator(kubeConfigFile)
if err != nil {
return nil, err
}
return ToMiddleware(auth), nil
}
type webhookAuth struct {
auth authenticator.Token
}
func (w *webhookAuth) Authenticate(req *http.Request) (user.Info, bool, error) {
token := req.Header.Get("Authorization")
if strings.HasPrefix(token, "Bearer ") {
token = strings.TrimPrefix(token, "Bearer ")
} else {
token = ""
}
if token == "" {
cookie, err := req.Cookie("R_SESS")
if err != nil && err != http.ErrNoCookie {
return nil, false, err
} else if err != http.ErrNoCookie && len(cookie.Value) > 0 {
token = "cookie://" + cookie.Value
}
}
if token == "" {
return nil, false, nil
}
resp, ok, err := w.auth.AuthenticateToken(req.Context(), token)
if resp == nil {
return nil, ok, err
}
return resp.User, ok, err
}
func ToMiddleware(auth Authenticator) func(rw http.ResponseWriter, req *http.Request, next http.Handler) {
return func(rw http.ResponseWriter, req *http.Request, next http.Handler) {
info, ok, err := auth.Authenticate(req)
if err != nil {
rw.WriteHeader(http.StatusServiceUnavailable)
rw.Write([]byte(err.Error()))
return
}
if !ok {
rw.WriteHeader(http.StatusUnauthorized)
return
}
ctx := request.WithUser(req.Context(), info)
req = req.WithContext(ctx)
req.Header.Set(v1.ImpersonateUserHeader, info.GetName())
for _, group := range info.GetGroups() {
req.Header.Set(v1.ImpersonateGroupHeader, group)
}
next.ServeHTTP(rw, req)
}
}

View File

@@ -22,7 +22,7 @@ func (o Object) Map(names ...string) Object {
func (o Object) Slice(names ...string) (result []Object) {
v := values.GetValueN(o, names...)
for _, item := range convert.ToInterfaceSlice(v) {
result = append(result, Object(convert.ToMapInterface(item)))
result = append(result, convert.ToMapInterface(item))
}
return
}

View File

@@ -1,17 +1,22 @@
package proxy
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
types2 "k8s.io/apimachinery/pkg/types"
"github.com/rancher/norman/pkg/types/convert"
errors2 "github.com/pkg/errors"
"github.com/rancher/norman/pkg/types"
"github.com/rancher/norman/pkg/types/convert/merge"
"github.com/rancher/norman/pkg/types/values"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/watch"
@@ -260,31 +265,32 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.Schema, params typ
return types.APIObject{}, err
}
for i := 0; i < 5; i++ {
resp, err := k8sClient.Get(id, metav1.GetOptions{})
if apiOp.Method == http.MethodPatch {
bytes, err := json.Marshal(data)
if err != nil {
return types.APIObject{}, err
}
resourceVersion, existing := resp.GetResourceVersion(), resp.Object
existing = merge.APIUpdateMerge(schema.InternalSchema, apiOp.Schemas, existing, data, apiOp.Option("replace") == "true")
values.PutValue(existing, resourceVersion, "metadata", "resourceVersion")
if len(apiOp.Namespaces) > 0 {
values.PutValue(existing, apiOp.Namespaces[0], "metadata", "namespace")
}
values.PutValue(existing, id, "metadata", "name")
resp, err = k8sClient.Update(&unstructured.Unstructured{Object: existing}, metav1.UpdateOptions{})
if errors.IsConflict(err) {
continue
} else if err != nil {
resp, err := k8sClient.Patch(id, types2.StrategicMergePatchType, bytes, metav1.PatchOptions{})
if err != nil {
return types.APIObject{}, err
}
_, result, err = s.singleResult(apiOp, schema, resp)
return types.ToAPI(result), err
}
resourceVersion := convert.ToString(values.GetValueN(data, "metadata", "resourceVersion"))
if resourceVersion == "" {
return types.APIObject{}, fmt.Errorf("metadata.resourceVersion is required for update")
}
resp, err := k8sClient.Update(&unstructured.Unstructured{Object: data}, metav1.UpdateOptions{})
if err != nil {
return types.APIObject{}, err
}
_, result, err = s.singleResult(apiOp, schema, resp)
return types.ToAPI(result), err
}

View File

@@ -1,156 +0,0 @@
package merge
import (
"strings"
"github.com/rancher/norman/pkg/types"
convert2 "github.com/rancher/norman/pkg/types/convert"
definition2 "github.com/rancher/norman/pkg/types/definition"
)
func APIUpdateMerge(schema *types.Schema, schemas *types.Schemas, dest, src map[string]interface{}, replace bool) map[string]interface{} {
result := UpdateMerge(schema, schemas, dest, src, replace)
if s, ok := dest["status"]; ok {
result["status"] = s
}
if m, ok := dest["metadata"]; ok {
result["metadata"] = mergeMetadata(convert2.ToMapInterface(m), convert2.ToMapInterface(src["metadata"]))
}
return result
}
func UpdateMerge(schema *types.Schema, schemas *types.Schemas, dest, src map[string]interface{}, replace bool) map[string]interface{} {
return mergeMaps("", nil, schema, schemas, replace, dest, src)
}
func isProtected(k string) bool {
if !strings.Contains(k, "cattle.io/") || (isField(k) && k != "field.cattle.io/creatorId") {
return false
}
return true
}
func isField(k string) bool {
return strings.HasPrefix(k, "field.cattle.io/")
}
func mergeProtected(dest, src map[string]interface{}) map[string]interface{} {
if src == nil {
return dest
}
result := copyMap(dest)
for k, v := range src {
if isProtected(k) {
continue
}
result[k] = v
}
for k := range dest {
if isProtected(k) || isField(k) {
continue
}
if _, ok := src[k]; !ok {
delete(result, k)
}
}
return result
}
func mergeMetadata(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
result := copyMap(dest)
labels := convert2.ToMapInterface(dest["labels"])
srcLabels := convert2.ToMapInterface(src["labels"])
labels = mergeProtected(labels, srcLabels)
annotations := convert2.ToMapInterface(dest["annotations"])
srcAnnotation := convert2.ToMapInterface(src["annotations"])
annotations = mergeProtected(annotations, srcAnnotation)
result["labels"] = labels
result["annotations"] = annotations
return result
}
func merge(field, fieldType string, parentSchema, schema *types.Schema, schemas *types.Schemas, replace bool, dest, src interface{}) interface{} {
if isMap(field, schema, schemas) {
return src
}
sm, smOk := src.(map[string]interface{})
dm, dmOk := dest.(map[string]interface{})
if smOk && dmOk {
fieldType, fieldSchema := getSchema(field, fieldType, parentSchema, schema, schemas)
return mergeMaps(fieldType, schema, fieldSchema, schemas, replace, dm, sm)
}
return src
}
func getSchema(field, parentFieldType string, parentSchema, schema *types.Schema, schemas *types.Schemas) (string, *types.Schema) {
if schema == nil {
if definition2.IsMapType(parentFieldType) && parentSchema != nil {
subType := definition2.SubType(parentFieldType)
s := schemas.Schema(subType)
if s != nil && s.InternalSchema != nil {
s = s.InternalSchema
}
return subType, s
}
return "", nil
}
fieldType := schema.ResourceFields[field].Type
s := schemas.Schema(fieldType)
if s != nil && s.InternalSchema != nil {
return fieldType, s.InternalSchema
}
return fieldType, s
}
func isMap(field string, schema *types.Schema, schemas *types.Schemas) bool {
if schema == nil {
return false
}
f := schema.ResourceFields[field]
mapType := definition2.IsMapType(f.Type)
if !mapType {
return false
}
subType := definition2.SubType(f.Type)
return schemas.Schema(subType) == nil
}
func mergeMaps(fieldType string, parentSchema, schema *types.Schema, schemas *types.Schemas, replace bool, dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
result := copyMapReplace(schema, dest, replace)
for k, v := range src {
result[k] = merge(k, fieldType, parentSchema, schema, schemas, replace, dest[k], v)
}
return result
}
func copyMap(src map[string]interface{}) map[string]interface{} {
result := map[string]interface{}{}
for k, v := range src {
result[k] = v
}
return result
}
func copyMapReplace(schema *types.Schema, src map[string]interface{}, replace bool) map[string]interface{} {
result := map[string]interface{}{}
for k, v := range src {
if replace && schema != nil {
f := schema.ResourceFields[k]
if f.Update {
continue
}
}
result[k] = v
}
return result
}

View File

@@ -45,7 +45,14 @@ func getHost(r *http.Request, scheme string) string {
func getScheme(r *http.Request) string {
scheme := r.Header.Get(ForwardedProtoHeader)
if scheme != "" {
return scheme
switch scheme {
case "ws":
return "http"
case "wss":
return "https"
default:
return scheme
}
} else if r.TLS != nil {
return "https"
}

View File

@@ -1081,7 +1081,7 @@ type LabelSelector struct {
MatchLabels map[string]string `json:"matchLabels,omitempty" protobuf:"bytes,1,rep,name=matchLabels"`
// matchExpressions is a list of label selector requirements. The requirements are ANDed.
// +optional
MatchExpressions []LabelSelectorRequirement `json:",omitempty" protobuf:"bytes,2,rep,name=matchExpressions"`
MatchExpressions []LabelSelectorRequirement `json:"matchExpressions,omitempty" protobuf:"bytes,2,rep,name=matchExpressions"`
}
// A label selector requirement is a selector that contains values, a key, and an operator that

View File

@@ -0,0 +1,90 @@
/*
Copyright 2018 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 authenticator
import (
"context"
"fmt"
"net/http"
)
func authenticate(ctx context.Context, implicitAuds Audiences, authenticate func() (*Response, bool, error)) (*Response, bool, error) {
targetAuds, ok := AudiencesFrom(ctx)
// We can remove this once api audiences is never empty. That will probably
// be N releases after TokenRequest is GA.
if !ok {
return authenticate()
}
auds := implicitAuds.Intersect(targetAuds)
if len(auds) == 0 {
return nil, false, nil
}
resp, ok, err := authenticate()
if err != nil || !ok {
return nil, false, err
}
if len(resp.Audiences) > 0 {
// maybe the authenticator was audience aware after all.
return nil, false, fmt.Errorf("audience agnostic authenticator wrapped an authenticator that returned audiences: %q", resp.Audiences)
}
resp.Audiences = auds
return resp, true, nil
}
type audAgnosticRequestAuthenticator struct {
implicit Audiences
delegate Request
}
var _ = Request(&audAgnosticRequestAuthenticator{})
func (a *audAgnosticRequestAuthenticator) AuthenticateRequest(req *http.Request) (*Response, bool, error) {
return authenticate(req.Context(), a.implicit, func() (*Response, bool, error) {
return a.delegate.AuthenticateRequest(req)
})
}
// WrapAudienceAgnosticRequest wraps an audience agnostic request authenticator
// to restrict its accepted audiences to a set of implicit audiences.
func WrapAudienceAgnosticRequest(implicit Audiences, delegate Request) Request {
return &audAgnosticRequestAuthenticator{
implicit: implicit,
delegate: delegate,
}
}
type audAgnosticTokenAuthenticator struct {
implicit Audiences
delegate Token
}
var _ = Token(&audAgnosticTokenAuthenticator{})
func (a *audAgnosticTokenAuthenticator) AuthenticateToken(ctx context.Context, tok string) (*Response, bool, error) {
return authenticate(ctx, a.implicit, func() (*Response, bool, error) {
return a.delegate.AuthenticateToken(ctx, tok)
})
}
// WrapAudienceAgnosticToken wraps an audience agnostic token authenticator to
// restrict its accepted audiences to a set of implicit audiences.
func WrapAudienceAgnosticToken(implicit Audiences, delegate Token) Token {
return &audAgnosticTokenAuthenticator{
implicit: implicit,
delegate: delegate,
}
}

View File

@@ -0,0 +1,63 @@
/*
Copyright 2018 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 authenticator
import "context"
// Audiences is a container for the Audiences of a token.
type Audiences []string
// The key type is unexported to prevent collisions
type key int
const (
// audiencesKey is the context key for request audiences.
audiencesKey key = iota
)
// WithAudiences returns a context that stores a request's expected audiences.
func WithAudiences(ctx context.Context, auds Audiences) context.Context {
return context.WithValue(ctx, audiencesKey, auds)
}
// AudiencesFrom returns a request's expected audiences stored in the request context.
func AudiencesFrom(ctx context.Context) (Audiences, bool) {
auds, ok := ctx.Value(audiencesKey).(Audiences)
return auds, ok
}
// Has checks if Audiences contains a specific audiences.
func (a Audiences) Has(taud string) bool {
for _, aud := range a {
if aud == taud {
return true
}
}
return false
}
// Intersect intersects Audiences with a target Audiences and returns all
// elements in both.
func (a Audiences) Intersect(tauds Audiences) Audiences {
selected := Audiences{}
for _, taud := range tauds {
if a.Has(taud) {
selected = append(selected, taud)
}
}
return selected
}

View File

@@ -0,0 +1,80 @@
/*
Copyright 2014 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 authenticator
import (
"context"
"net/http"
"k8s.io/apiserver/pkg/authentication/user"
)
// Token checks a string value against a backing authentication store and
// returns a Response or an error if the token could not be checked.
type Token interface {
AuthenticateToken(ctx context.Context, token string) (*Response, bool, error)
}
// Request attempts to extract authentication information from a request and
// returns a Response or an error if the request could not be checked.
type Request interface {
AuthenticateRequest(req *http.Request) (*Response, bool, error)
}
// Password checks a username and password against a backing authentication
// store and returns a Response or an error if the password could not be
// checked.
type Password interface {
AuthenticatePassword(ctx context.Context, user, password string) (*Response, bool, error)
}
// TokenFunc is a function that implements the Token interface.
type TokenFunc func(ctx context.Context, token string) (*Response, bool, error)
// AuthenticateToken implements authenticator.Token.
func (f TokenFunc) AuthenticateToken(ctx context.Context, token string) (*Response, bool, error) {
return f(ctx, token)
}
// RequestFunc is a function that implements the Request interface.
type RequestFunc func(req *http.Request) (*Response, bool, error)
// AuthenticateRequest implements authenticator.Request.
func (f RequestFunc) AuthenticateRequest(req *http.Request) (*Response, bool, error) {
return f(req)
}
// PasswordFunc is a function that implements the Password interface.
type PasswordFunc func(ctx context.Context, user, password string) (*Response, bool, error)
// AuthenticatePassword implements authenticator.Password.
func (f PasswordFunc) AuthenticatePassword(ctx context.Context, user, password string) (*Response, bool, error) {
return f(ctx, user, password)
}
// Response is the struct returned by authenticator interfaces upon successful
// authentication. It contains information about whether the authenticator
// authenticated the request, information about the context of the
// authentication, and information about the authenticated user.
type Response struct {
// Audiences is the set of audiences the authenticator was able to validate
// the token against. If the authenticator is not audience aware, this field
// will be empty.
Audiences Audiences
// User is the UserInfo associated with the authentication context.
User user.Info
}

View File

@@ -0,0 +1,211 @@
/*
Copyright 2017 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 webhook
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
// AuthenticationInfoResolverWrapper can be used to inject Dial function to the
// rest.Config generated by the resolver.
type AuthenticationInfoResolverWrapper func(AuthenticationInfoResolver) AuthenticationInfoResolver
// NewDefaultAuthenticationInfoResolverWrapper builds a default authn resolver wrapper
func NewDefaultAuthenticationInfoResolverWrapper(
proxyTransport *http.Transport,
kubeapiserverClientConfig *rest.Config) AuthenticationInfoResolverWrapper {
webhookAuthResolverWrapper := func(delegate AuthenticationInfoResolver) AuthenticationInfoResolver {
return &AuthenticationInfoResolverDelegator{
ClientConfigForFunc: func(server string) (*rest.Config, error) {
if server == "kubernetes.default.svc" {
return kubeapiserverClientConfig, nil
}
return delegate.ClientConfigFor(server)
},
ClientConfigForServiceFunc: func(serviceName, serviceNamespace string) (*rest.Config, error) {
if serviceName == "kubernetes" && serviceNamespace == corev1.NamespaceDefault {
return kubeapiserverClientConfig, nil
}
ret, err := delegate.ClientConfigForService(serviceName, serviceNamespace)
if err != nil {
return nil, err
}
if proxyTransport != nil && proxyTransport.DialContext != nil {
ret.Dial = proxyTransport.DialContext
}
return ret, err
},
}
}
return webhookAuthResolverWrapper
}
// AuthenticationInfoResolver builds rest.Config base on the server or service
// name and service namespace.
type AuthenticationInfoResolver interface {
// ClientConfigFor builds rest.Config based on the server.
ClientConfigFor(server string) (*rest.Config, error)
// ClientConfigForService builds rest.Config based on the serviceName and
// serviceNamespace.
ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error)
}
// AuthenticationInfoResolverDelegator implements AuthenticationInfoResolver.
type AuthenticationInfoResolverDelegator struct {
ClientConfigForFunc func(server string) (*rest.Config, error)
ClientConfigForServiceFunc func(serviceName, serviceNamespace string) (*rest.Config, error)
}
// ClientConfigFor returns client config for given server.
func (a *AuthenticationInfoResolverDelegator) ClientConfigFor(server string) (*rest.Config, error) {
return a.ClientConfigForFunc(server)
}
// ClientConfigForService returns client config for given service.
func (a *AuthenticationInfoResolverDelegator) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
return a.ClientConfigForServiceFunc(serviceName, serviceNamespace)
}
type defaultAuthenticationInfoResolver struct {
kubeconfig clientcmdapi.Config
}
// NewDefaultAuthenticationInfoResolver generates an AuthenticationInfoResolver
// that builds rest.Config based on the kubeconfig file. kubeconfigFile is the
// path to the kubeconfig.
func NewDefaultAuthenticationInfoResolver(kubeconfigFile string) (AuthenticationInfoResolver, error) {
if len(kubeconfigFile) == 0 {
return &defaultAuthenticationInfoResolver{}, nil
}
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeconfigFile
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err := loader.RawConfig()
if err != nil {
return nil, err
}
return &defaultAuthenticationInfoResolver{kubeconfig: clientConfig}, nil
}
func (c *defaultAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
return c.clientConfig(server)
}
func (c *defaultAuthenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
return c.clientConfig(serviceName + "." + serviceNamespace + ".svc")
}
func (c *defaultAuthenticationInfoResolver) clientConfig(target string) (*rest.Config, error) {
// exact match
if authConfig, ok := c.kubeconfig.AuthInfos[target]; ok {
return restConfigFromKubeconfig(authConfig)
}
// star prefixed match
serverSteps := strings.Split(target, ".")
for i := 1; i < len(serverSteps); i++ {
nickName := "*." + strings.Join(serverSteps[i:], ".")
if authConfig, ok := c.kubeconfig.AuthInfos[nickName]; ok {
return restConfigFromKubeconfig(authConfig)
}
}
// if we're trying to hit the kube-apiserver and there wasn't an explicit config, use the in-cluster config
if target == "kubernetes.default.svc" {
// if we can find an in-cluster-config use that. If we can't, fall through.
inClusterConfig, err := rest.InClusterConfig()
if err == nil {
return setGlobalDefaults(inClusterConfig), nil
}
}
// star (default) match
if authConfig, ok := c.kubeconfig.AuthInfos["*"]; ok {
return restConfigFromKubeconfig(authConfig)
}
// use the current context from the kubeconfig if possible
if len(c.kubeconfig.CurrentContext) > 0 {
if currContext, ok := c.kubeconfig.Contexts[c.kubeconfig.CurrentContext]; ok {
if len(currContext.AuthInfo) > 0 {
if currAuth, ok := c.kubeconfig.AuthInfos[currContext.AuthInfo]; ok {
return restConfigFromKubeconfig(currAuth)
}
}
}
}
// anonymous
return setGlobalDefaults(&rest.Config{}), nil
}
func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Config, error) {
config := &rest.Config{}
// blindly overwrite existing values based on precedence
if len(configAuthInfo.Token) > 0 {
config.BearerToken = configAuthInfo.Token
} else if len(configAuthInfo.TokenFile) > 0 {
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
if err != nil {
return nil, err
}
config.BearerToken = string(tokenBytes)
config.BearerTokenFile = configAuthInfo.TokenFile
}
if len(configAuthInfo.Impersonate) > 0 {
config.Impersonate = rest.ImpersonationConfig{
UserName: configAuthInfo.Impersonate,
Groups: configAuthInfo.ImpersonateGroups,
Extra: configAuthInfo.ImpersonateUserExtra,
}
}
if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 {
config.CertFile = configAuthInfo.ClientCertificate
config.CertData = configAuthInfo.ClientCertificateData
config.KeyFile = configAuthInfo.ClientKey
config.KeyData = configAuthInfo.ClientKeyData
}
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
config.Username = configAuthInfo.Username
config.Password = configAuthInfo.Password
}
if configAuthInfo.AuthProvider != nil {
return nil, fmt.Errorf("auth provider not supported")
}
return setGlobalDefaults(config), nil
}
func setGlobalDefaults(config *rest.Config) *rest.Config {
config.UserAgent = "kube-apiserver-admission"
config.Timeout = 30 * time.Second
return config
}

198
vendor/k8s.io/apiserver/pkg/util/webhook/client.go generated vendored Normal file
View File

@@ -0,0 +1,198 @@
/*
Copyright 2017 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 webhook
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"github.com/hashicorp/golang-lru"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/rest"
)
const (
defaultCacheSize = 200
)
// ClientConfig defines parameters required for creating a hook client.
type ClientConfig struct {
Name string
URL string
CABundle []byte
Service *ClientConfigService
}
// ClientConfigService defines service discovery parameters of the webhook.
type ClientConfigService struct {
Name string
Namespace string
Path string
}
// ClientManager builds REST clients to talk to webhooks. It caches the clients
// to avoid duplicate creation.
type ClientManager struct {
authInfoResolver AuthenticationInfoResolver
serviceResolver ServiceResolver
negotiatedSerializer runtime.NegotiatedSerializer
cache *lru.Cache
}
// NewClientManager creates a clientManager.
func NewClientManager(gv schema.GroupVersion, addToSchemaFunc func(s *runtime.Scheme) error) (ClientManager, error) {
cache, err := lru.New(defaultCacheSize)
if err != nil {
return ClientManager{}, err
}
hookScheme := runtime.NewScheme()
if err := addToSchemaFunc(hookScheme); err != nil {
return ClientManager{}, err
}
return ClientManager{
cache: cache,
negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
Serializer: serializer.NewCodecFactory(hookScheme).LegacyCodec(gv),
}),
}, nil
}
// SetAuthenticationInfoResolverWrapper sets the
// AuthenticationInfoResolverWrapper.
func (cm *ClientManager) SetAuthenticationInfoResolverWrapper(wrapper AuthenticationInfoResolverWrapper) {
if wrapper != nil {
cm.authInfoResolver = wrapper(cm.authInfoResolver)
}
}
// SetAuthenticationInfoResolver sets the AuthenticationInfoResolver.
func (cm *ClientManager) SetAuthenticationInfoResolver(resolver AuthenticationInfoResolver) {
cm.authInfoResolver = resolver
}
// SetServiceResolver sets the ServiceResolver.
func (cm *ClientManager) SetServiceResolver(sr ServiceResolver) {
if sr != nil {
cm.serviceResolver = sr
}
}
// Validate checks if ClientManager is properly set up.
func (cm *ClientManager) Validate() error {
var errs []error
if cm.negotiatedSerializer == nil {
errs = append(errs, fmt.Errorf("the clientManager requires a negotiatedSerializer"))
}
if cm.serviceResolver == nil {
errs = append(errs, fmt.Errorf("the clientManager requires a serviceResolver"))
}
if cm.authInfoResolver == nil {
errs = append(errs, fmt.Errorf("the clientManager requires an authInfoResolver"))
}
return utilerrors.NewAggregate(errs)
}
// HookClient get a RESTClient from the cache, or constructs one based on the
// webhook configuration.
func (cm *ClientManager) HookClient(cc ClientConfig) (*rest.RESTClient, error) {
ccWithNoName := cc
ccWithNoName.Name = ""
cacheKey, err := json.Marshal(ccWithNoName)
if err != nil {
return nil, err
}
if client, ok := cm.cache.Get(string(cacheKey)); ok {
return client.(*rest.RESTClient), nil
}
complete := func(cfg *rest.Config) (*rest.RESTClient, error) {
// Combine CAData from the config with any existing CA bundle provided
if len(cfg.TLSClientConfig.CAData) > 0 {
cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, '\n')
}
cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, cc.CABundle...)
cfg.ContentConfig.NegotiatedSerializer = cm.negotiatedSerializer
cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
client, err := rest.UnversionedRESTClientFor(cfg)
if err == nil {
cm.cache.Add(string(cacheKey), client)
}
return client, err
}
if cc.Service != nil {
restConfig, err := cm.authInfoResolver.ClientConfigForService(cc.Service.Name, cc.Service.Namespace)
if err != nil {
return nil, err
}
cfg := rest.CopyConfig(restConfig)
serverName := cc.Service.Name + "." + cc.Service.Namespace + ".svc"
host := serverName + ":443"
cfg.Host = "https://" + host
cfg.APIPath = cc.Service.Path
// Set the server name if not already set
if len(cfg.TLSClientConfig.ServerName) == 0 {
cfg.TLSClientConfig.ServerName = serverName
}
delegateDialer := cfg.Dial
if delegateDialer == nil {
var d net.Dialer
delegateDialer = d.DialContext
}
cfg.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) {
if addr == host {
u, err := cm.serviceResolver.ResolveEndpoint(cc.Service.Namespace, cc.Service.Name)
if err != nil {
return nil, err
}
addr = u.Host
}
return delegateDialer(ctx, network, addr)
}
return complete(cfg)
}
if cc.URL == "" {
return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: errors.New("webhook configuration must have either service or URL")}
}
u, err := url.Parse(cc.URL)
if err != nil {
return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)}
}
restConfig, err := cm.authInfoResolver.ClientConfigFor(u.Host)
if err != nil {
return nil, err
}
cfg := rest.CopyConfig(restConfig)
cfg.Host = u.Scheme + "://" + u.Host
cfg.APIPath = u.Path
return complete(cfg)
}

34
vendor/k8s.io/apiserver/pkg/util/webhook/error.go generated vendored Normal file
View File

@@ -0,0 +1,34 @@
/*
Copyright 2017 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 webhook
import "fmt"
// ErrCallingWebhook is returned for transport-layer errors calling webhooks. It
// represents a failure to talk to the webhook, not the webhook rejecting a
// request.
type ErrCallingWebhook struct {
WebhookName string
Reason error
}
func (e *ErrCallingWebhook) Error() string {
if e.Reason != nil {
return fmt.Sprintf("failed calling webhook %q: %v", e.WebhookName, e.Reason)
}
return fmt.Sprintf("failed calling webhook %q; no further details available", e.WebhookName)
}

107
vendor/k8s.io/apiserver/pkg/util/webhook/gencerts.sh generated vendored Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Copyright 2017 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.
set -e
# gencerts.sh generates the certificates for the webhook tests.
#
# It is not expected to be run often (there is no go generate rule), and mainly
# exists for documentation purposes.
CN_BASE="webhook_tests"
cat > server.conf << EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
EOF
cat > client.conf << EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
EOF
# Create a certificate authority
openssl genrsa -out caKey.pem 2048
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca"
# Create a second certificate authority
openssl genrsa -out badCAKey.pem 2048
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=${CN_BASE}_ca"
# Create a server certiticate
openssl genrsa -out serverKey.pem 2048
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=${CN_BASE}_server" -config server.conf
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
# Create a client certiticate
openssl genrsa -out clientKey.pem 2048
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
outfile=certs_test.go
cat > $outfile << EOF
/*
Copyright 2017 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.
*/
// This file was generated using openssl by the gencerts.sh script
// and holds raw certificates for the webhook tests.
package webhook
EOF
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
data=$(cat ${file}.pem)
echo "" >> $outfile
echo "var $file = []byte(\`$data\`)" >> $outfile
done
# Clean up after we're done.
rm ./*.pem
rm ./*.csr
rm ./*.srl
rm ./*.conf

View File

@@ -0,0 +1,46 @@
/*
Copyright 2017 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 webhook
import (
"errors"
"fmt"
"net/url"
)
// ServiceResolver knows how to convert a service reference into an actual location.
type ServiceResolver interface {
ResolveEndpoint(namespace, name string) (*url.URL, error)
}
type defaultServiceResolver struct{}
// NewDefaultServiceResolver creates a new default server resolver.
func NewDefaultServiceResolver() ServiceResolver {
return &defaultServiceResolver{}
}
// ResolveEndpoint constructs a service URL from a given namespace and name
// note that the name and namespace are required and by default all created addresses use HTTPS scheme.
// for example:
// name=ross namespace=andromeda resolves to https://ross.andromeda.svc:443
func (sr defaultServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
if len(name) == 0 || len(namespace) == 0 {
return nil, errors.New("cannot resolve an empty service name or namespace")
}
return &url.URL{Scheme: "https", Host: fmt.Sprintf("%s.%s.svc:443", name, namespace)}, nil
}

101
vendor/k8s.io/apiserver/pkg/util/webhook/validation.go generated vendored Normal file
View File

@@ -0,0 +1,101 @@
/*
Copyright 2018 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 webhook
import (
"fmt"
"net/url"
"strings"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ValidateWebhookURL validates webhook's URL.
func ValidateWebhookURL(fldPath *field.Path, URL string, forceHttps bool) field.ErrorList {
var allErrors field.ErrorList
const form = "; desired format: https://host[/path]"
if u, err := url.Parse(URL); err != nil {
allErrors = append(allErrors, field.Required(fldPath, "url must be a valid URL: "+err.Error()+form))
} else {
if forceHttps && u.Scheme != "https" {
allErrors = append(allErrors, field.Invalid(fldPath, u.Scheme, "'https' is the only allowed URL scheme"+form))
}
if len(u.Host) == 0 {
allErrors = append(allErrors, field.Invalid(fldPath, u.Host, "host must be provided"+form))
}
if u.User != nil {
allErrors = append(allErrors, field.Invalid(fldPath, u.User.String(), "user information is not permitted in the URL"))
}
if len(u.Fragment) != 0 {
allErrors = append(allErrors, field.Invalid(fldPath, u.Fragment, "fragments are not permitted in the URL"))
}
if len(u.RawQuery) != 0 {
allErrors = append(allErrors, field.Invalid(fldPath, u.RawQuery, "query parameters are not permitted in the URL"))
}
}
return allErrors
}
func ValidateWebhookService(fldPath *field.Path, namespace, name string, path *string) field.ErrorList {
var allErrors field.ErrorList
if len(name) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("name"), "service name is required"))
}
if len(namespace) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("namespace"), "service namespace is required"))
}
if path == nil {
return allErrors
}
// TODO: replace below with url.Parse + verifying that host is empty?
urlPath := *path
if urlPath == "/" || len(urlPath) == 0 {
return allErrors
}
if urlPath == "//" {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "segment[0] may not be empty"))
return allErrors
}
if !strings.HasPrefix(urlPath, "/") {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "must start with a '/'"))
}
urlPathToCheck := urlPath[1:]
if strings.HasSuffix(urlPathToCheck, "/") {
urlPathToCheck = urlPathToCheck[:len(urlPathToCheck)-1]
}
steps := strings.Split(urlPathToCheck, "/")
for i, step := range steps {
if len(step) == 0 {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d] may not be empty", i)))
continue
}
failures := validation.IsDNS1123Subdomain(step)
for _, failure := range failures {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d]: %v", i, failure)))
}
}
return allErrors
}

120
vendor/k8s.io/apiserver/pkg/util/webhook/webhook.go generated vendored Normal file
View File

@@ -0,0 +1,120 @@
/*
Copyright 2016 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 webhook implements a generic HTTP webhook plugin.
package webhook
import (
"fmt"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// defaultRequestTimeout is set for all webhook request. This is the absolute
// timeout of the HTTP request, including reading the response body.
const defaultRequestTimeout = 30 * time.Second
type GenericWebhook struct {
RestClient *rest.RESTClient
InitialBackoff time.Duration
}
// NewGenericWebhook creates a new GenericWebhook from the provided kubeconfig file.
func NewGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, initialBackoff time.Duration) (*GenericWebhook, error) {
return newGenericWebhook(scheme, codecFactory, kubeConfigFile, groupVersions, initialBackoff, defaultRequestTimeout)
}
func newGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, initialBackoff, requestTimeout time.Duration) (*GenericWebhook, error) {
for _, groupVersion := range groupVersions {
if !scheme.IsVersionRegistered(groupVersion) {
return nil, fmt.Errorf("webhook plugin requires enabling extension resource: %s", groupVersion)
}
}
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeConfigFile
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err := loader.ClientConfig()
if err != nil {
return nil, err
}
// Kubeconfigs can't set a timeout, this can only be set through a command line flag.
//
// https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/overrides.go
//
// Set this to something reasonable so request to webhooks don't hang forever.
clientConfig.Timeout = requestTimeout
codec := codecFactory.LegacyCodec(groupVersions...)
clientConfig.ContentConfig.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
restClient, err := rest.UnversionedRESTClientFor(clientConfig)
if err != nil {
return nil, err
}
return &GenericWebhook{restClient, initialBackoff}, nil
}
// WithExponentialBackoff will retry webhookFn() up to 5 times with exponentially increasing backoff when
// it returns an error for which apierrors.SuggestsClientDelay() or apierrors.IsInternalError() returns true.
func (g *GenericWebhook) WithExponentialBackoff(webhookFn func() rest.Result) rest.Result {
var result rest.Result
WithExponentialBackoff(g.InitialBackoff, func() error {
result = webhookFn()
return result.Error()
})
return result
}
// WithExponentialBackoff will retry webhookFn() up to 5 times with exponentially increasing backoff when
// it returns an error for which apierrors.SuggestsClientDelay() or apierrors.IsInternalError() returns true.
func WithExponentialBackoff(initialBackoff time.Duration, webhookFn func() error) error {
backoff := wait.Backoff{
Duration: initialBackoff,
Factor: 1.5,
Jitter: 0.2,
Steps: 5,
}
var err error
wait.ExponentialBackoff(backoff, func() (bool, error) {
err = webhookFn()
// these errors indicate a transient error that should be retried.
if net.IsConnectionReset(err) || apierrors.IsInternalError(err) || apierrors.IsTimeout(err) || apierrors.IsTooManyRequests(err) {
return false, nil
}
// if the error sends the Retry-After header, we respect it as an explicit confirmation we should retry.
if _, shouldRetry := apierrors.SuggestsClientDelay(err); shouldRetry {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
})
return err
}

View File

@@ -0,0 +1,177 @@
/*
Copyright 2016 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 webhook implements the authenticator.Token interface using HTTP webhooks.
package webhook
import (
"context"
"errors"
"time"
authentication "k8s.io/api/authentication/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/kubernetes/scheme"
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
"k8s.io/klog"
)
var (
groupVersions = []schema.GroupVersion{authentication.SchemeGroupVersion}
)
const retryBackoff = 500 * time.Millisecond
// Ensure WebhookTokenAuthenticator implements the authenticator.Token interface.
var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil)
type WebhookTokenAuthenticator struct {
tokenReview authenticationclient.TokenReviewInterface
initialBackoff time.Duration
implicitAuds authenticator.Audiences
}
// NewFromInterface creates a webhook authenticator using the given tokenReview
// client. It is recommend to wrap this authenticator with the token cache
// authenticator implemented in
// k8s.io/apiserver/pkg/authentication/token/cache.
func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
}
// New creates a new WebhookTokenAuthenticator from the provided kubeconfig
// file. It is recommend to wrap this authenticator with the token cache
// authenticator implemented in
// k8s.io/apiserver/pkg/authentication/token/cache.
func New(kubeConfigFile string, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile)
if err != nil {
return nil, err
}
return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
}
// newWithBackoff allows tests to skip the sleep.
func newWithBackoff(tokenReview authenticationclient.TokenReviewInterface, initialBackoff time.Duration, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
return &WebhookTokenAuthenticator{tokenReview, initialBackoff, implicitAuds}, nil
}
// AuthenticateToken implements the authenticator.Token interface.
func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
// We take implicit audiences of the API server at WebhookTokenAuthenticator
// construction time. The outline of how we validate audience here is:
//
// * if the ctx is not audience limited, don't do any audience validation.
// * if ctx is audience-limited, add the audiences to the tokenreview spec
// * if the tokenreview returns with audiences in the status that intersect
// with the audiences in the ctx, copy into the response and return success
// * if the tokenreview returns without an audience in the status, ensure
// the ctx audiences intersect with the implicit audiences, and set the
// intersection in the response.
// * otherwise return unauthenticated.
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
r := &authentication.TokenReview{
Spec: authentication.TokenReviewSpec{
Token: token,
Audiences: wantAuds,
},
}
var (
result *authentication.TokenReview
err error
auds authenticator.Audiences
)
webhook.WithExponentialBackoff(w.initialBackoff, func() error {
result, err = w.tokenReview.Create(r)
return err
})
if err != nil {
// An error here indicates bad configuration or an outage. Log for debugging.
klog.Errorf("Failed to make webhook authenticator request: %v", err)
return nil, false, err
}
if checkAuds {
gotAuds := w.implicitAuds
if len(result.Status.Audiences) > 0 {
gotAuds = result.Status.Audiences
}
auds = wantAuds.Intersect(gotAuds)
if len(auds) == 0 {
return nil, false, nil
}
}
r.Status = result.Status
if !r.Status.Authenticated {
var err error
if len(r.Status.Error) != 0 {
err = errors.New(r.Status.Error)
}
return nil, false, err
}
var extra map[string][]string
if r.Status.User.Extra != nil {
extra = map[string][]string{}
for k, v := range r.Status.User.Extra {
extra[k] = v
}
}
return &authenticator.Response{
User: &user.DefaultInfo{
Name: r.Status.User.Username,
UID: r.Status.User.UID,
Groups: r.Status.User.Groups,
Extra: extra,
},
Audiences: auds,
}, true, nil
}
// tokenReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
// and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string) (authenticationclient.TokenReviewInterface, error) {
localScheme := runtime.NewScheme()
if err := scheme.AddToScheme(localScheme); err != nil {
return nil, err
}
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
return nil, err
}
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
if err != nil {
return nil, err
}
return &tokenReviewClient{gw}, nil
}
type tokenReviewClient struct {
w *webhook.GenericWebhook
}
func (t *tokenReviewClient) Create(tokenReview *authentication.TokenReview) (*authentication.TokenReview, error) {
result := &authentication.TokenReview{}
err := t.w.RestClient.Post().Body(tokenReview).Do().Into(result)
return result, err
}

11
vendor/modules.txt vendored
View File

@@ -55,12 +55,12 @@ github.com/rancher/norman/pkg/data
github.com/rancher/norman/pkg/store/empty
github.com/rancher/norman/pkg/types/values
github.com/rancher/norman/pkg/api/builtin
github.com/rancher/norman/pkg/auth
github.com/rancher/norman/pkg/api
github.com/rancher/norman/pkg/urlbuilder
github.com/rancher/norman/pkg/httperror
github.com/rancher/norman/pkg/types/slice
github.com/rancher/norman/pkg/types/definition
github.com/rancher/norman/pkg/types/convert/merge
github.com/rancher/norman/pkg/api/writer
github.com/rancher/norman/pkg/store/schema
github.com/rancher/norman/pkg/api/access
@@ -133,6 +133,8 @@ gopkg.in/yaml.v2
# k8s.io/api v0.0.0 => ../kuberlite/staging/src/k8s.io/api
k8s.io/api/rbac/v1
k8s.io/api/core/v1
k8s.io/api/authorization/v1
k8s.io/api/authentication/v1
k8s.io/api/admissionregistration/v1beta1
k8s.io/api/apps/v1
k8s.io/api/apps/v1beta1
@@ -163,9 +165,7 @@ k8s.io/api/settings/v1alpha1
k8s.io/api/storage/v1
k8s.io/api/storage/v1alpha1
k8s.io/api/storage/v1beta1
k8s.io/api/authentication/v1
k8s.io/api/authentication/v1beta1
k8s.io/api/authorization/v1
k8s.io/api/authorization/v1beta1
# k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8
k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1
@@ -227,7 +227,10 @@ k8s.io/apimachinery/pkg/apis/meta/v1beta1
# k8s.io/apiserver v0.0.0-20190409021813-1ec86e4da56c
k8s.io/apiserver/pkg/authentication/user
k8s.io/apiserver/pkg/endpoints/request
k8s.io/apiserver/pkg/authentication/authenticator
k8s.io/apiserver/plugin/pkg/authenticator/token/webhook
k8s.io/apiserver/pkg/apis/audit
k8s.io/apiserver/pkg/util/webhook
# k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible => ../kuberlite/staging/src/k8s.io/client-go
k8s.io/client-go/dynamic
k8s.io/client-go/rest
@@ -235,6 +238,7 @@ k8s.io/client-go/dynamic/dynamicinformer
k8s.io/client-go/tools/cache
k8s.io/client-go/util/workqueue
k8s.io/client-go/discovery
k8s.io/client-go/kubernetes/typed/authorization/v1
k8s.io/client-go/transport
k8s.io/client-go/kubernetes
k8s.io/client-go/informers/rbac/v1
@@ -263,7 +267,6 @@ k8s.io/client-go/kubernetes/typed/apps/v1beta2
k8s.io/client-go/kubernetes/typed/auditregistration/v1alpha1
k8s.io/client-go/kubernetes/typed/authentication/v1
k8s.io/client-go/kubernetes/typed/authentication/v1beta1
k8s.io/client-go/kubernetes/typed/authorization/v1
k8s.io/client-go/kubernetes/typed/authorization/v1beta1
k8s.io/client-go/kubernetes/typed/autoscaling/v1
k8s.io/client-go/kubernetes/typed/autoscaling/v2beta1