diff --git a/pkg/accesscontrol/access_control.go b/pkg/accesscontrol/access_control.go index cba03fd..9e29cdb 100644 --- a/pkg/accesscontrol/access_control.go +++ b/pkg/accesscontrol/access_control.go @@ -8,7 +8,7 @@ import ( ) type AccessControl struct { - server.AllAccess + server.SchemaBasedAccess } func NewAccessControl() *AccessControl { diff --git a/pkg/auth/filter.go b/pkg/auth/filter.go index 059fd6d..5ee30ec 100644 --- a/pkg/auth/filter.go +++ b/pkg/auth/filter.go @@ -13,6 +13,7 @@ import ( "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" ) type Authenticator interface { @@ -125,7 +126,7 @@ func (w *webhookAuth) Authenticate(req *http.Request) (user.Info, bool, error) { return resp.User, ok, err } -func ToMiddleware(auth Authenticator) func(rw http.ResponseWriter, req *http.Request, next http.Handler) { +func ToMiddleware(auth Authenticator) Middleware { return func(rw http.ResponseWriter, req *http.Request, next http.Handler) { info, ok, err := auth.Authenticate(req) if err != nil { @@ -144,3 +145,24 @@ func ToMiddleware(auth Authenticator) func(rw http.ResponseWriter, req *http.Req next.ServeHTTP(rw, req) } } + +func Impersonation(req *http.Request) (user.Info, bool, error) { + userName := req.Header.Get(transport.ImpersonateUserHeader) + if userName == "" { + return nil, false, nil + } + + result := user.DefaultInfo{ + Name: userName, + Groups: req.Header[transport.ImpersonateGroupHeader], + Extra: map[string][]string{}, + } + + for k, v := range req.Header { + if strings.HasPrefix(k, transport.ImpersonateUserExtraHeaderPrefix) { + result.Extra[k[len(transport.ImpersonateUserExtraHeaderPrefix):]] = v + } + } + + return &result, true, nil +} diff --git a/pkg/client/factory.go b/pkg/client/factory.go index 702415d..d7c34c0 100644 --- a/pkg/client/factory.go +++ b/pkg/client/factory.go @@ -1,39 +1,43 @@ package client import ( + "fmt" "time" "github.com/rancher/steve/pkg/attributes" "github.com/rancher/steve/pkg/schemaserver/types" + "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" ) type Factory struct { - client dynamic.Interface - watchClient dynamic.Interface - Config *rest.Config + impersonate bool + clientCfg *rest.Config + watchClientCfg *rest.Config + client dynamic.Interface + Config *rest.Config } -func NewFactory(cfg *rest.Config) (*Factory, error) { - newCfg := rest.CopyConfig(cfg) - newCfg.QPS = 10000 - newCfg.Burst = 100 - c, err := dynamic.NewForConfig(newCfg) +func NewFactory(cfg *rest.Config, impersonate bool) (*Factory, error) { + clientCfg := rest.CopyConfig(cfg) + clientCfg.QPS = 10000 + clientCfg.Burst = 100 + + watchClientCfg := rest.CopyConfig(cfg) + watchClientCfg.Timeout = 30 * time.Minute + + dc, err := dynamic.NewForConfig(watchClientCfg) if err != nil { return nil, err } - newCfg = rest.CopyConfig(cfg) - newCfg.Timeout = 30 * time.Minute - wc, err := dynamic.NewForConfig(newCfg) - if err != nil { - return nil, err - } return &Factory{ - client: c, - watchClient: wc, - Config: newCfg, + client: dc, + impersonate: impersonate, + clientCfg: clientCfg, + watchClientCfg: watchClientCfg, + Config: watchClientCfg, }, nil } @@ -42,11 +46,30 @@ func (p *Factory) DynamicClient() dynamic.Interface { } func (p *Factory) Client(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) { - gvr := attributes.GVR(s) - return p.client.Resource(gvr).Namespace(namespace), nil + return p.newClient(ctx, p.clientCfg, s, namespace) } func (p *Factory) ClientForWatch(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) { - gvr := attributes.GVR(s) - return p.watchClient.Resource(gvr).Namespace(namespace), nil + return p.newClient(ctx, p.watchClientCfg, s, namespace) +} + +func (p *Factory) newClient(ctx *types.APIRequest, cfg *rest.Config, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) { + if p.impersonate { + user, ok := request.UserFrom(ctx.Context()) + if !ok { + return nil, fmt.Errorf("user not found for impersonation") + } + cfg = rest.CopyConfig(cfg) + cfg.Impersonate.UserName = user.GetName() + cfg.Impersonate.Groups = user.GetGroups() + cfg.Impersonate.Extra = user.GetExtra() + } + + client, err := dynamic.NewForConfig(cfg) + if err != nil { + return nil, err + } + + gvr := attributes.GVR(s) + return client.Resource(gvr).Namespace(namespace), nil } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index ca658a4..382cc83 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -7,7 +7,9 @@ import ( "strings" "github.com/rancher/wrangler/pkg/kubeconfig" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/proxy" + "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/client-go/rest" "k8s.io/client-go/transport" ) @@ -23,6 +25,34 @@ func HandlerFromConfig(prefix, kubeConfig string) (http.Handler, error) { return Handler(prefix, cfg) } +func ImpersonatingHandler(prefix string, cfg *rest.Config) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + impersonate(rw, req, prefix, cfg) + }) +} + +func impersonate(rw http.ResponseWriter, req *http.Request, prefix string, cfg *rest.Config) { + user, ok := request.UserFrom(req.Context()) + if !ok { + rw.WriteHeader(http.StatusUnauthorized) + return + } + + cfg = rest.CopyConfig(cfg) + cfg.Impersonate.UserName = user.GetName() + cfg.Impersonate.Groups = user.GetGroups() + cfg.Impersonate.Extra = user.GetExtra() + + handler, err := Handler(prefix, cfg) + if err != nil { + logrus.Errorf("failed to impersonate %v for proxy: %v", user, err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + handler.ServeHTTP(rw, req) +} + // Mostly copied from "kubectl proxy" code func Handler(prefix string, cfg *rest.Config) (http.Handler, error) { host := cfg.Host diff --git a/pkg/schemaserver/server/access.go b/pkg/schemaserver/server/access.go index e0461e9..06de94d 100644 --- a/pkg/schemaserver/server/access.go +++ b/pkg/schemaserver/server/access.go @@ -9,49 +9,49 @@ import ( "github.com/rancher/wrangler/pkg/slice" ) -type AllAccess struct { +type SchemaBasedAccess struct { } -func (*AllAccess) CanCreate(apiOp *types.APIRequest, schema *types.APISchema) error { +func (*SchemaBasedAccess) CanCreate(apiOp *types.APIRequest, schema *types.APISchema) error { if slice.ContainsString(schema.CollectionMethods, http.MethodPost) { return nil } return httperror.NewAPIError(validation.PermissionDenied, "can not create "+schema.ID) } -func (*AllAccess) CanGet(apiOp *types.APIRequest, schema *types.APISchema) error { +func (*SchemaBasedAccess) CanGet(apiOp *types.APIRequest, schema *types.APISchema) error { if slice.ContainsString(schema.ResourceMethods, http.MethodGet) { return nil } return httperror.NewAPIError(validation.PermissionDenied, "can not get "+schema.ID) } -func (*AllAccess) CanList(apiOp *types.APIRequest, schema *types.APISchema) error { +func (*SchemaBasedAccess) CanList(apiOp *types.APIRequest, schema *types.APISchema) error { if slice.ContainsString(schema.CollectionMethods, http.MethodGet) { return nil } return httperror.NewAPIError(validation.PermissionDenied, "can not list "+schema.ID) } -func (*AllAccess) CanUpdate(apiOp *types.APIRequest, obj types.APIObject, schema *types.APISchema) error { +func (*SchemaBasedAccess) CanUpdate(apiOp *types.APIRequest, obj types.APIObject, schema *types.APISchema) error { if slice.ContainsString(schema.ResourceMethods, http.MethodPut) { return nil } return httperror.NewAPIError(validation.PermissionDenied, "can not update "+schema.ID) } -func (*AllAccess) CanDelete(apiOp *types.APIRequest, obj types.APIObject, schema *types.APISchema) error { +func (*SchemaBasedAccess) CanDelete(apiOp *types.APIRequest, obj types.APIObject, schema *types.APISchema) error { if slice.ContainsString(schema.ResourceMethods, http.MethodDelete) { return nil } return httperror.NewAPIError(validation.PermissionDenied, "can not delete "+schema.ID) } -func (a *AllAccess) CanWatch(apiOp *types.APIRequest, schema *types.APISchema) error { +func (a *SchemaBasedAccess) CanWatch(apiOp *types.APIRequest, schema *types.APISchema) error { return a.CanList(apiOp, schema) } -func (*AllAccess) CanAction(apiOp *types.APIRequest, schema *types.APISchema, name string) error { +func (*SchemaBasedAccess) CanAction(apiOp *types.APIRequest, schema *types.APISchema, name string) error { if _, ok := schema.ActionHandlers[name]; ok { return httperror.NewAPIError(validation.PermissionDenied, "no such action "+name) } diff --git a/pkg/schemaserver/server/server.go b/pkg/schemaserver/server/server.go index 8f1cf78..3426a4f 100644 --- a/pkg/schemaserver/server/server.go +++ b/pkg/schemaserver/server/server.go @@ -19,12 +19,12 @@ type RequestHandler interface { } type Server struct { - ResponseWriters map[string]types.ResponseWriter - Schemas *types.APISchemas - Defaults Defaults - AccessControl types.AccessControl - Parser parse.Parser - URLParser parse.URLParser + ResponseWriters map[string]types.ResponseWriter + Schemas *types.APISchemas + Defaults Defaults + AccessControl types.AccessControl + Parser parse.Parser + URLParser parse.URLParser } type Defaults struct { @@ -39,7 +39,7 @@ type Defaults struct { func DefaultAPIServer() *Server { s := &Server{ - Schemas: types.EmptyAPISchemas(), + Schemas: types.EmptyAPISchemas(), ResponseWriters: map[string]types.ResponseWriter{ "json": &writer.EncodingResponseWriter{ ContentType: "application/json", @@ -56,7 +56,7 @@ func DefaultAPIServer() *Server { Encoder: types.YAMLEncoder, }, }, - AccessControl: &AllAccess{}, + AccessControl: &SchemaBasedAccess{}, Defaults: Defaults{ ByIDHandler: handlers.ByIDHandler, CreateHandler: handlers.CreateHandler, @@ -140,8 +140,8 @@ func (s *Server) GetSchemas() *types.APISchemas { func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { s.Handle(&types.APIRequest{ - Request: req, - Response: rw, + Request: req, + Response: rw, }) } diff --git a/pkg/schemaserver/types/types.go b/pkg/schemaserver/types/types.go index 2219143..98b8e84 100644 --- a/pkg/schemaserver/types/types.go +++ b/pkg/schemaserver/types/types.go @@ -75,8 +75,21 @@ type APISchema struct { Store Store `json:"-"` } +func copyHandlers(m map[string]http.Handler) map[string]http.Handler { + if m == nil { + return nil + } + result := make(map[string]http.Handler, len(m)) + for k, v := range m { + result[k] = v + } + + return result +} func (a *APISchema) DeepCopy() *APISchema { r := *a + r.ActionHandlers = copyHandlers(a.ActionHandlers) + r.LinkHandlers = copyHandlers(a.ActionHandlers) r.Schema = r.Schema.DeepCopy() return &r } diff --git a/pkg/server/handler/apiserver.go b/pkg/server/handler/apiserver.go index 6281f4d..f4cb357 100644 --- a/pkg/server/handler/apiserver.go +++ b/pkg/server/handler/apiserver.go @@ -18,7 +18,8 @@ import ( func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, next http.Handler, routerFunc router.RouterFunc) (http.Handler, error) { var ( - err error + proxy http.Handler + err error ) a := &apiServer{ @@ -27,9 +28,13 @@ func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, ne } a.server.AccessControl = accesscontrol.NewAccessControl() - proxy, err := k8sproxy.Handler("/", cfg) - if err != nil { - return nil, err + if authMiddleware == nil { + proxy, err = k8sproxy.Handler("/", cfg) + if err != nil { + return nil, err + } + } else { + proxy = k8sproxy.ImpersonatingHandler("/", cfg) } w := authMiddleware.Wrap diff --git a/pkg/server/server.go b/pkg/server/server.go index 262355f..3f7abf1 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -51,7 +51,7 @@ func setup(ctx context.Context, server *Server) (http.Handler, *schema.Collectio return nil, nil, err } - cf, err := client.NewFactory(server.RestConfig) + cf, err := client.NewFactory(server.RestConfig, server.AuthMiddleware != nil) if err != nil { return nil, nil, err }