diff --git a/main.go b/main.go index f76873b..71ccc0c 100644 --- a/main.go +++ b/main.go @@ -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", diff --git a/pkg/controllers/schema/schemas.go b/pkg/controllers/schema/schemas.go index d75824a..5e57f8f 100644 --- a/pkg/controllers/schema/schemas.go +++ b/pkg/controllers/schema/schemas.go @@ -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 diff --git a/pkg/server/server.go b/pkg/server/server.go index 89bfa92..0872299 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -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 } diff --git a/vendor/github.com/rancher/norman/pkg/api/server.go b/vendor/github.com/rancher/norman/pkg/api/server.go index acca3c8..41e08b9 100644 --- a/vendor/github.com/rancher/norman/pkg/api/server.go +++ b/vendor/github.com/rancher/norman/pkg/api/server.go @@ -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: diff --git a/vendor/github.com/rancher/norman/pkg/auth/filter.go b/vendor/github.com/rancher/norman/pkg/auth/filter.go new file mode 100644 index 0000000..584fe29 --- /dev/null +++ b/vendor/github.com/rancher/norman/pkg/auth/filter.go @@ -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) + } +} diff --git a/vendor/github.com/rancher/norman/pkg/data/data.go b/vendor/github.com/rancher/norman/pkg/data/data.go index f728248..defb545 100644 --- a/vendor/github.com/rancher/norman/pkg/data/data.go +++ b/vendor/github.com/rancher/norman/pkg/data/data.go @@ -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 } diff --git a/vendor/github.com/rancher/norman/pkg/store/proxy/proxy_store.go b/vendor/github.com/rancher/norman/pkg/store/proxy/proxy_store.go index 574a90f..a921656 100644 --- a/vendor/github.com/rancher/norman/pkg/store/proxy/proxy_store.go +++ b/vendor/github.com/rancher/norman/pkg/store/proxy/proxy_store.go @@ -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 } diff --git a/vendor/github.com/rancher/norman/pkg/types/convert/merge/merge.go b/vendor/github.com/rancher/norman/pkg/types/convert/merge/merge.go deleted file mode 100644 index 8ba1939..0000000 --- a/vendor/github.com/rancher/norman/pkg/types/convert/merge/merge.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/rancher/norman/pkg/urlbuilder/base.go b/vendor/github.com/rancher/norman/pkg/urlbuilder/base.go index 9df945a..a7a6da3 100644 --- a/vendor/github.com/rancher/norman/pkg/urlbuilder/base.go +++ b/vendor/github.com/rancher/norman/pkg/urlbuilder/base.go @@ -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" } diff --git a/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go b/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go index dfc74e3..46ef65f 100644 --- a/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go +++ b/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go @@ -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 diff --git a/vendor/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go b/vendor/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go new file mode 100644 index 0000000..bcf7eb4 --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go @@ -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, + } +} diff --git a/vendor/k8s.io/apiserver/pkg/authentication/authenticator/audiences.go b/vendor/k8s.io/apiserver/pkg/authentication/authenticator/audiences.go new file mode 100644 index 0000000..2a3a918 --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/authentication/authenticator/audiences.go @@ -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 +} diff --git a/vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go b/vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go new file mode 100644 index 0000000..e3b1b62 --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go @@ -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 +} diff --git a/vendor/k8s.io/apiserver/pkg/util/webhook/authentication.go b/vendor/k8s.io/apiserver/pkg/util/webhook/authentication.go new file mode 100644 index 0000000..dd0f4e5 --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/util/webhook/authentication.go @@ -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 +} diff --git a/vendor/k8s.io/apiserver/pkg/util/webhook/client.go b/vendor/k8s.io/apiserver/pkg/util/webhook/client.go new file mode 100644 index 0000000..0766bcd --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/util/webhook/client.go @@ -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) +} diff --git a/vendor/k8s.io/apiserver/pkg/util/webhook/error.go b/vendor/k8s.io/apiserver/pkg/util/webhook/error.go new file mode 100644 index 0000000..4701530 --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/util/webhook/error.go @@ -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) +} diff --git a/vendor/k8s.io/apiserver/pkg/util/webhook/gencerts.sh b/vendor/k8s.io/apiserver/pkg/util/webhook/gencerts.sh new file mode 100644 index 0000000..de40773 --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/util/webhook/gencerts.sh @@ -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 diff --git a/vendor/k8s.io/apiserver/pkg/util/webhook/serviceresolver.go b/vendor/k8s.io/apiserver/pkg/util/webhook/serviceresolver.go new file mode 100644 index 0000000..41684dd --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/util/webhook/serviceresolver.go @@ -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 +} diff --git a/vendor/k8s.io/apiserver/pkg/util/webhook/validation.go b/vendor/k8s.io/apiserver/pkg/util/webhook/validation.go new file mode 100644 index 0000000..2ddb2c0 --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/util/webhook/validation.go @@ -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 +} diff --git a/vendor/k8s.io/apiserver/pkg/util/webhook/webhook.go b/vendor/k8s.io/apiserver/pkg/util/webhook/webhook.go new file mode 100644 index 0000000..eb6c17b --- /dev/null +++ b/vendor/k8s.io/apiserver/pkg/util/webhook/webhook.go @@ -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 +} diff --git a/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go b/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go new file mode 100644 index 0000000..e13985d --- /dev/null +++ b/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e3f6ba4..56c6984 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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