//TestUserContext validates that a userinfo can be get/set on a context object func TestUserContext(t *testing.T) { ctx := genericapirequest.NewContext() _, ok := genericapirequest.UserFrom(ctx) if ok { t.Fatalf("Should not be ok because there is no user.Info on the context") } ctx = genericapirequest.WithUser( ctx, &user.DefaultInfo{ Name: "bob", UID: "123", Groups: []string{"group1"}, Extra: map[string][]string{"foo": {"bar"}}, }, ) result, ok := genericapirequest.UserFrom(ctx) if !ok { t.Fatalf("Error getting user info") } expectedName := "bob" if result.GetName() != expectedName { t.Fatalf("Get user name error, Expected: %s, Actual: %s", expectedName, result.GetName()) } expectedUID := "123" if result.GetUID() != expectedUID { t.Fatalf("Get UID error, Expected: %s, Actual: %s", expectedUID, result.GetName()) } expectedGroup := "group1" actualGroup := result.GetGroups() if len(actualGroup) != 1 { t.Fatalf("Get user group number error, Expected: 1, Actual: %d", len(actualGroup)) } else if actualGroup[0] != expectedGroup { t.Fatalf("Get user group error, Expected: %s, Actual: %s", expectedGroup, actualGroup[0]) } expectedExtraKey := "foo" expectedExtraValue := "bar" actualExtra := result.GetExtra() if len(actualExtra[expectedExtraKey]) != 1 { t.Fatalf("Get user extra map number error, Expected: 1, Actual: %d", len(actualExtra[expectedExtraKey])) } else if actualExtra[expectedExtraKey][0] != expectedExtraValue { t.Fatalf("Get user extra map value error, Expected: %s, Actual: %s", expectedExtraValue, actualExtra[expectedExtraKey]) } }
func GetAuthorizerAttributes(ctx request.Context) (authorizer.Attributes, error) { attribs := authorizer.AttributesRecord{} user, ok := request.UserFrom(ctx) if ok { attribs.User = user } requestInfo, found := request.RequestInfoFrom(ctx) if !found { return nil, errors.New("no RequestInfo found in the context") } // Start with common attributes that apply to resource and non-resource requests attribs.ResourceRequest = requestInfo.IsResourceRequest attribs.Path = requestInfo.Path attribs.Verb = requestInfo.Verb attribs.APIGroup = requestInfo.APIGroup attribs.APIVersion = requestInfo.APIVersion attribs.Resource = requestInfo.Resource attribs.Subresource = requestInfo.Subresource attribs.Namespace = requestInfo.Namespace attribs.Name = requestInfo.Name return &attribs, nil }
func (r *REST) Create(ctx genericapirequest.Context, obj runtime.Object) (runtime.Object, error) { selfSAR, ok := obj.(*authorizationapi.SelfSubjectAccessReview) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SelfSubjectAccessReview: %#v", obj)) } if errs := authorizationvalidation.ValidateSelfSubjectAccessReview(selfSAR); len(errs) > 0 { return nil, apierrors.NewInvalid(authorizationapi.Kind(selfSAR.Kind), "", errs) } userToCheck, exists := genericapirequest.UserFrom(ctx) if !exists { return nil, apierrors.NewBadRequest("no user present on request") } var authorizationAttributes authorizer.AttributesRecord if selfSAR.Spec.ResourceAttributes != nil { authorizationAttributes = authorizationutil.ResourceAttributesFrom(userToCheck, *selfSAR.Spec.ResourceAttributes) } else { authorizationAttributes = authorizationutil.NonResourceAttributesFrom(userToCheck, *selfSAR.Spec.NonResourceAttributes) } allowed, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes) selfSAR.Status = authorizationapi.SubjectAccessReviewStatus{ Allowed: allowed, Reason: reason, } if evaluationErr != nil { selfSAR.Status.EvaluationError = evaluationErr.Error() } return selfSAR, nil }
// ConfirmNoEscalation determines if the roles for a given user in a given namespace encompass the provided role. func ConfirmNoEscalation(ctx genericapirequest.Context, ruleResolver AuthorizationRuleResolver, rules []rbac.PolicyRule) error { ruleResolutionErrors := []error{} user, ok := genericapirequest.UserFrom(ctx) if !ok { return fmt.Errorf("no user on context") } namespace, _ := genericapirequest.NamespaceFrom(ctx) ownerRules, err := ruleResolver.RulesFor(user, namespace) if err != nil { // As per AuthorizationRuleResolver contract, this may return a non fatal error with an incomplete list of policies. Log the error and continue. glog.V(1).Infof("non-fatal error getting local rules for %v: %v", user, err) ruleResolutionErrors = append(ruleResolutionErrors, err) } ownerRightsCover, missingRights := Covers(ownerRules, rules) if !ownerRightsCover { user, _ := genericapirequest.UserFrom(ctx) return apierrors.NewUnauthorized(fmt.Sprintf("attempt to grant extra privileges: %v user=%v ownerrules=%v ruleResolutionErrors=%v", missingRights, user, ownerRules, ruleResolutionErrors)) } return nil }
// PrepareForCreate clears fields that are not allowed to be set by end users // on creation. func (csrStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { csr := obj.(*certificates.CertificateSigningRequest) // Clear any user-specified info csr.Spec.Username = "" csr.Spec.UID = "" csr.Spec.Groups = nil // Inject user.Info from request context if user, ok := genericapirequest.UserFrom(ctx); ok { csr.Spec.Username = user.GetName() csr.Spec.UID = user.GetUID() csr.Spec.Groups = user.GetGroups() } // Be explicit that users cannot create pre-approved certificate requests. csr.Status = certificates.CertificateSigningRequestStatus{} csr.Status.Conditions = []certificates.CertificateSigningRequestCondition{} }
func EscalationAllowed(ctx genericapirequest.Context) bool { u, ok := genericapirequest.UserFrom(ctx) if !ok { // the only way to be without a user is to either have no authenticators by explicitly saying that's your preference // or to be connecting via the insecure port, in which case this logically doesn't apply return true } // system:masters is special because the API server uses it for privileged loopback connections // therefore we know that a member of system:masters can always do anything for _, group := range u.GetGroups() { if group == user.SystemPrivilegedGroup { return true } } return false }
// BindingAuthorized returns true if the user associated with the context is explicitly authorized to bind the specified roleRef func BindingAuthorized(ctx genericapirequest.Context, roleRef rbac.RoleRef, bindingNamespace string, a authorizer.Authorizer) bool { if a == nil { return false } user, ok := genericapirequest.UserFrom(ctx) if !ok { return false } attrs := authorizer.AttributesRecord{ User: user, Verb: "bind", // check against the namespace where the binding is being created (or the empty namespace for clusterrolebindings). // this allows delegation to bind particular clusterroles in rolebindings within particular namespaces, // and to authorize binding a clusterrole across all namespaces in a clusterrolebinding. Namespace: bindingNamespace, ResourceRequest: true, } // This occurs after defaulting and conversion, so values pulled from the roleRef won't change // Invalid APIGroup or Name values will fail validation switch roleRef.Kind { case "ClusterRole": attrs.APIGroup = roleRef.APIGroup attrs.Resource = "clusterroles" attrs.Name = roleRef.Name case "Role": attrs.APIGroup = roleRef.APIGroup attrs.Resource = "roles" attrs.Name = roleRef.Name default: return false } ok, _, err := a.Authorize(attrs) if err != nil { utilruntime.HandleError(fmt.Errorf( "error authorizing user %#v to bind %#v in namespace %s: %v", roleRef, bindingNamespace, user, err, )) } return ok }
func TestAuthenticateRequest(t *testing.T) { success := make(chan struct{}) contextMapper := genericapirequest.NewRequestContextMapper() auth := WithAuthentication( http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { ctx, ok := contextMapper.Get(req) if ctx == nil || !ok { t.Errorf("no context stored on contextMapper: %#v", contextMapper) } user, ok := genericapirequest.UserFrom(ctx) if user == nil || !ok { t.Errorf("no user stored in context: %#v", ctx) } if req.Header.Get("Authorization") != "" { t.Errorf("Authorization header should be removed from request on success: %#v", req) } close(success) }), contextMapper, authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { if req.Header.Get("Authorization") == "Something" { return &user.DefaultInfo{Name: "user"}, true, nil } return nil, false, errors.New("Authorization header is missing.") }), http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { t.Errorf("unexpected call to failed") }), ) auth.ServeHTTP(httptest.NewRecorder(), &http.Request{Header: map[string][]string{"Authorization": {"Something"}}}) <-success empty, err := genericapirequest.IsEmpty(contextMapper) if err != nil { t.Fatalf("unexpected error: %v", err) } if !empty { t.Fatalf("contextMapper should have no stored requests: %v", contextMapper) } }
// ConnectResource returns a function that handles a connect request on a rest.Storage object. func ConnectResource(connecter rest.Connecter, scope RequestScope, admit admission.Interface, restPath string) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter namespace, name, err := scope.Namer.Name(req) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } ctx := scope.ContextFunc(req) ctx = request.WithNamespace(ctx, namespace) opts, subpath, subpathKey := connecter.NewConnectOptions() if err := getRequestOptions(req, scope, opts, subpath, subpathKey); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } if admit.Handles(admission.Connect) { connectRequest := &rest.ConnectRequest{ Name: name, Options: opts, ResourcePath: restPath, } userInfo, _ := request.UserFrom(ctx) err = admit.Admit(admission.NewAttributesRecord(connectRequest, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Connect, userInfo)) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } } handler, err := connecter.Connect(ctx, name, opts, &responder{scope: scope, req: req, res: res}) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } handler.ServeHTTP(w, req.Request) } }
func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface, includeName bool) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { // For performance tracking purposes. trace := util.NewTrace("Create " + req.Request.URL.Path) defer trace.LogIfLong(500 * time.Millisecond) w := res.ResponseWriter // TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer) timeout := parseTimeout(req.Request.URL.Query().Get("timeout")) var ( namespace, name string err error ) if includeName { namespace, name, err = scope.Namer.Name(req) } else { namespace, err = scope.Namer.Namespace(req) } if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } ctx := scope.ContextFunc(req) ctx = request.WithNamespace(ctx, namespace) gv := scope.Kind.GroupVersion() s, err := negotiation.NegotiateInputSerializer(req.Request, scope.Serializer) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } decoder := scope.Serializer.DecoderToVersion(s.Serializer, schema.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}) body, err := readBody(req.Request) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } defaultGVK := scope.Kind original := r.New() trace.Step("About to convert to expected version") obj, gvk, err := decoder.Decode(body, &defaultGVK, original) if err != nil { err = transformDecodeError(typer, err, original, gvk, body) scope.err(err, res.ResponseWriter, req.Request) return } if gvk.GroupVersion() != gv { err = errors.NewBadRequest(fmt.Sprintf("the API version in the data (%s) does not match the expected API version (%v)", gvk.GroupVersion().String(), gv.String())) scope.err(err, res.ResponseWriter, req.Request) return } trace.Step("Conversion done") if admit != nil && admit.Handles(admission.Create) { userInfo, _ := request.UserFrom(ctx) err = admit.Admit(admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, userInfo)) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } } trace.Step("About to store object in database") result, err := finishRequest(timeout, func() (runtime.Object, error) { out, err := r.Create(ctx, name, obj) if status, ok := out.(*metav1.Status); ok && err == nil && status.Code == 0 { status.Code = http.StatusCreated } return out, err }) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } trace.Step("Object stored in database") if err := setSelfLink(result, req, scope.Namer); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } trace.Step("Self-link added") responsewriters.WriteObject(http.StatusCreated, scope.Kind.GroupVersion(), scope.Serializer, result, w, req.Request) } }
func TestImpersonationFilter(t *testing.T) { testCases := []struct { name string user user.Info impersonationUser string impersonationGroups []string impersonationUserExtras map[string][]string expectedUser user.Info expectedCode int }{ { name: "not-impersonating", user: &user.DefaultInfo{ Name: "tester", }, expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusOK, }, { name: "impersonating-error", user: &user.DefaultInfo{ Name: "tester", }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusForbidden, }, { name: "impersonating-group-without-user", user: &user.DefaultInfo{ Name: "tester", }, impersonationGroups: []string{"some-group"}, expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusInternalServerError, }, { name: "impersonating-extra-without-user", user: &user.DefaultInfo{ Name: "tester", }, impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusInternalServerError, }, { name: "disallowed-group", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, expectedCode: http.StatusForbidden, }, { name: "allowed-group", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "group-impersonater"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, expectedUser: &user.DefaultInfo{ Name: "system:admin", Groups: []string{"some-group"}, Extra: map[string][]string{}, }, expectedCode: http.StatusOK, }, { name: "disallowed-userextra-1", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, expectedCode: http.StatusForbidden, }, { name: "disallowed-userextra-2", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-project"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-project"}, }, expectedCode: http.StatusForbidden, }, { name: "disallowed-userextra-3", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-particular-scopes"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-particular-scopes"}, }, expectedCode: http.StatusForbidden, }, { name: "allowed-userextras", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-scopes"}, }, impersonationUser: "******", impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}}, expectedUser: &user.DefaultInfo{ Name: "system:admin", Groups: []string{}, Extra: map[string][]string{"scopes": {"scope-a", "scope-b"}}, }, expectedCode: http.StatusOK, }, { name: "allowed-users-impersonation", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"regular-impersonater"}, }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "tester", Groups: []string{}, Extra: map[string][]string{}, }, expectedCode: http.StatusOK, }, { name: "disallowed-impersonating", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"sa-impersonater"}, }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"sa-impersonater"}, }, expectedCode: http.StatusForbidden, }, { name: "allowed-sa-impersonating", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"sa-impersonater"}, Extra: map[string][]string{}, }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "system:serviceaccount:foo:default", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo"}, Extra: map[string][]string{}, }, expectedCode: http.StatusOK, }, } requestContextMapper := request.NewRequestContextMapper() var ctx request.Context var actualUser user.Info var lock sync.Mutex doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { currentCtx, _ := requestContextMapper.Get(req) user, exists := request.UserFrom(currentCtx) if !exists { actualUser = nil return } actualUser = user }) handler := func(delegate http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer func() { if r := recover(); r != nil { t.Errorf("Recovered %v", r) } }() lock.Lock() defer lock.Unlock() requestContextMapper.Update(req, ctx) currentCtx, _ := requestContextMapper.Get(req) user, exists := request.UserFrom(currentCtx) if !exists { actualUser = nil return } else { actualUser = user } delegate.ServeHTTP(w, req) }) }(WithImpersonation(doNothingHandler, requestContextMapper, impersonateAuthorizer{})) handler = request.WithRequestContext(handler, requestContextMapper) server := httptest.NewServer(handler) defer server.Close() for _, tc := range testCases { func() { lock.Lock() defer lock.Unlock() ctx = request.WithUser(request.NewContext(), tc.user) }() req, err := http.NewRequest("GET", server.URL, nil) if err != nil { t.Errorf("%s: unexpected error: %v", tc.name, err) continue } req.Header.Add(authenticationapi.ImpersonateUserHeader, tc.impersonationUser) for _, group := range tc.impersonationGroups { req.Header.Add(authenticationapi.ImpersonateGroupHeader, group) } for extraKey, values := range tc.impersonationUserExtras { for _, value := range values { req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value) } } resp, err := http.DefaultClient.Do(req) if err != nil { t.Errorf("%s: unexpected error: %v", tc.name, err) continue } if resp.StatusCode != tc.expectedCode { t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode) continue } if !reflect.DeepEqual(actualUser, tc.expectedUser) { t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser) continue } } }
// DeleteCollection returns a function that will handle a collection deletion func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestScope, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter // TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer) timeout := parseTimeout(req.Request.URL.Query().Get("timeout")) namespace, err := scope.Namer.Namespace(req) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } ctx := scope.ContextFunc(req) ctx = request.WithNamespace(ctx, namespace) if admit != nil && admit.Handles(admission.Delete) { userInfo, _ := request.UserFrom(ctx) err = admit.Admit(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, "", scope.Resource, scope.Subresource, admission.Delete, userInfo)) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } } listOptions := api.ListOptions{} if err := scope.ParameterCodec.DecodeParameters(req.Request.URL.Query(), scope.Kind.GroupVersion(), &listOptions); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } // transform fields // TODO: DecodeParametersInto should do this. if listOptions.FieldSelector != nil { fn := func(label, value string) (newLabel, newValue string, err error) { return scope.Convertor.ConvertFieldLabel(scope.Kind.GroupVersion().String(), scope.Kind.Kind, label, value) } if listOptions.FieldSelector, err = listOptions.FieldSelector.Transform(fn); err != nil { // TODO: allow bad request to set field causes based on query parameters err = errors.NewBadRequest(err.Error()) scope.err(err, res.ResponseWriter, req.Request) return } } options := &api.DeleteOptions{} if checkBody { body, err := readBody(req.Request) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } if len(body) > 0 { s, err := negotiation.NegotiateInputSerializer(req.Request, scope.Serializer) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } defaultGVK := scope.Kind.GroupVersion().WithKind("DeleteOptions") obj, _, err := scope.Serializer.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } if obj != options { scope.err(fmt.Errorf("decoded object cannot be converted to DeleteOptions"), res.ResponseWriter, req.Request) return } } } result, err := finishRequest(timeout, func() (runtime.Object, error) { return r.DeleteCollection(ctx, options, &listOptions) }) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } // if the rest.Deleter returns a nil object, fill out a status. Callers may return a valid // object with the response. if result == nil { result = &metav1.Status{ Status: metav1.StatusSuccess, Code: http.StatusOK, Details: &metav1.StatusDetails{ Kind: scope.Kind.Kind, }, } } else { // when a non-status response is returned, set the self link if _, ok := result.(*metav1.Status); !ok { if _, err := setListSelfLink(result, req, scope.Namer); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } } } responsewriters.WriteObjectNegotiated(scope.Serializer, scope.Kind.GroupVersion(), w, req.Request, http.StatusOK, result) } }
// DeleteResource returns a function that will handle a resource deletion func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestScope, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { // For performance tracking purposes. trace := util.NewTrace("Delete " + req.Request.URL.Path) defer trace.LogIfLong(500 * time.Millisecond) w := res.ResponseWriter // TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer) timeout := parseTimeout(req.Request.URL.Query().Get("timeout")) namespace, name, err := scope.Namer.Name(req) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } ctx := scope.ContextFunc(req) ctx = request.WithNamespace(ctx, namespace) options := &api.DeleteOptions{} if allowsOptions { body, err := readBody(req.Request) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } if len(body) > 0 { s, err := negotiation.NegotiateInputSerializer(req.Request, scope.Serializer) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } defaultGVK := scope.Kind.GroupVersion().WithKind("DeleteOptions") obj, _, err := scope.Serializer.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } if obj != options { scope.err(fmt.Errorf("decoded object cannot be converted to DeleteOptions"), res.ResponseWriter, req.Request) return } } else { if values := req.Request.URL.Query(); len(values) > 0 { if err := scope.ParameterCodec.DecodeParameters(values, scope.Kind.GroupVersion(), options); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } } } } if admit != nil && admit.Handles(admission.Delete) { userInfo, _ := request.UserFrom(ctx) err = admit.Admit(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Delete, userInfo)) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } } trace.Step("About do delete object from database") result, err := finishRequest(timeout, func() (runtime.Object, error) { return r.Delete(ctx, name, options) }) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } trace.Step("Object deleted from database") // if the rest.Deleter returns a nil object, fill out a status. Callers may return a valid // object with the response. if result == nil { result = &metav1.Status{ Status: metav1.StatusSuccess, Code: http.StatusOK, Details: &metav1.StatusDetails{ Name: name, Kind: scope.Kind.Kind, }, } } else { // when a non-status response is returned, set the self link if _, ok := result.(*metav1.Status); !ok { if err := setSelfLink(result, req, scope.Namer); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } } } responsewriters.WriteObject(http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req.Request) } }
// UpdateResource returns a function that will handle a resource update func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { // For performance tracking purposes. trace := util.NewTrace("Update " + req.Request.URL.Path) defer trace.LogIfLong(500 * time.Millisecond) w := res.ResponseWriter // TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer) timeout := parseTimeout(req.Request.URL.Query().Get("timeout")) namespace, name, err := scope.Namer.Name(req) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } ctx := scope.ContextFunc(req) ctx = request.WithNamespace(ctx, namespace) body, err := readBody(req.Request) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } s, err := negotiation.NegotiateInputSerializer(req.Request, scope.Serializer) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } defaultGVK := scope.Kind original := r.New() trace.Step("About to convert to expected version") obj, gvk, err := scope.Serializer.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, original) if err != nil { err = transformDecodeError(typer, err, original, gvk, body) scope.err(err, res.ResponseWriter, req.Request) return } if gvk.GroupVersion() != defaultGVK.GroupVersion() { err = errors.NewBadRequest(fmt.Sprintf("the API version in the data (%s) does not match the expected API version (%s)", gvk.GroupVersion(), defaultGVK.GroupVersion())) scope.err(err, res.ResponseWriter, req.Request) return } trace.Step("Conversion done") if err := checkName(obj, name, namespace, scope.Namer); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } var transformers []rest.TransformFunc if admit != nil && admit.Handles(admission.Update) { transformers = append(transformers, func(ctx request.Context, newObj, oldObj runtime.Object) (runtime.Object, error) { userInfo, _ := request.UserFrom(ctx) return newObj, admit.Admit(admission.NewAttributesRecord(newObj, oldObj, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Update, userInfo)) }) } trace.Step("About to store object in database") wasCreated := false result, err := finishRequest(timeout, func() (runtime.Object, error) { obj, created, err := r.Update(ctx, name, rest.DefaultUpdatedObjectInfo(obj, scope.Copier, transformers...)) wasCreated = created return obj, err }) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } trace.Step("Object stored in database") if err := setSelfLink(result, req, scope.Namer); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } trace.Step("Self-link added") status := http.StatusOK if wasCreated { status = http.StatusCreated } responsewriters.WriteObject(status, scope.Kind.GroupVersion(), scope.Serializer, result, w, req.Request) } }
// PatchResource returns a function that will handle a resource patch // TODO: Eventually PatchResource should just use GuaranteedUpdate and this routine should be a bit cleaner func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface, converter runtime.ObjectConvertor) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter // TODO: we either want to remove timeout or document it (if we // document, move timeout out of this function and declare it in // api_installer) timeout := parseTimeout(req.Request.URL.Query().Get("timeout")) namespace, name, err := scope.Namer.Name(req) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } ctx := scope.ContextFunc(req) ctx = request.WithNamespace(ctx, namespace) versionedObj, err := converter.ConvertToVersion(r.New(), scope.Kind.GroupVersion()) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } // TODO: handle this in negotiation contentType := req.HeaderParameter("Content-Type") // Remove "; charset=" if included in header. if idx := strings.Index(contentType, ";"); idx > 0 { contentType = contentType[:idx] } patchType := types.PatchType(contentType) patchJS, err := readBody(req.Request) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { scope.err(fmt.Errorf("no serializer defined for JSON"), res.ResponseWriter, req.Request) return } gv := scope.Kind.GroupVersion() codec := runtime.NewCodec( scope.Serializer.EncoderForVersion(s.Serializer, gv), scope.Serializer.DecoderToVersion(s.Serializer, schema.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}), ) updateAdmit := func(updatedObject runtime.Object, currentObject runtime.Object) error { if admit != nil && admit.Handles(admission.Update) { userInfo, _ := request.UserFrom(ctx) return admit.Admit(admission.NewAttributesRecord(updatedObject, currentObject, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Update, userInfo)) } return nil } result, err := patchResource(ctx, updateAdmit, timeout, versionedObj, r, name, patchType, patchJS, scope.Namer, scope.Copier, scope.Resource, codec) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return } if err := setSelfLink(result, req, scope.Namer); err != nil { scope.err(err, res.ResponseWriter, req.Request) return } responsewriters.WriteObject(http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req.Request) } }
// WithImpersonation is a filter that will inspect and check requests that attempt to change the user.Info for their requests func WithImpersonation(handler http.Handler, requestContextMapper request.RequestContextMapper, a authorizer.Authorizer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { impersonationRequests, err := buildImpersonationRequests(req.Header) if err != nil { glog.V(4).Infof("%v", err) responsewriters.InternalError(w, req, err) return } if len(impersonationRequests) == 0 { handler.ServeHTTP(w, req) return } ctx, exists := requestContextMapper.Get(req) if !exists { responsewriters.InternalError(w, req, errors.New("no context found for request")) return } requestor, exists := request.UserFrom(ctx) if !exists { responsewriters.InternalError(w, req, errors.New("no user found for request")) return } // if groups are not specified, then we need to look them up differently depending on the type of user // if they are specified, then they are the authority groupsSpecified := len(req.Header[authenticationapi.ImpersonateGroupHeader]) > 0 // make sure we're allowed to impersonate each thing we're requesting. While we're iterating through, start building username // and group information username := "" groups := []string{} userExtra := map[string][]string{} for _, impersonationRequest := range impersonationRequests { actingAsAttributes := &authorizer.AttributesRecord{ User: requestor, Verb: "impersonate", APIGroup: impersonationRequest.GetObjectKind().GroupVersionKind().Group, Namespace: impersonationRequest.Namespace, Name: impersonationRequest.Name, ResourceRequest: true, } switch impersonationRequest.GetObjectKind().GroupVersionKind().GroupKind() { case api.Kind("ServiceAccount"): actingAsAttributes.Resource = "serviceaccounts" username = serviceaccount.MakeUsername(impersonationRequest.Namespace, impersonationRequest.Name) if !groupsSpecified { // if groups aren't specified for a service account, we know the groups because its a fixed mapping. Add them groups = serviceaccount.MakeGroupNames(impersonationRequest.Namespace, impersonationRequest.Name) } case api.Kind("User"): actingAsAttributes.Resource = "users" username = impersonationRequest.Name case api.Kind("Group"): actingAsAttributes.Resource = "groups" groups = append(groups, impersonationRequest.Name) case authenticationapi.Kind("UserExtra"): extraKey := impersonationRequest.FieldPath extraValue := impersonationRequest.Name actingAsAttributes.Resource = "userextras" actingAsAttributes.Subresource = extraKey userExtra[extraKey] = append(userExtra[extraKey], extraValue) default: glog.V(4).Infof("unknown impersonation request type: %v", impersonationRequest) responsewriters.Forbidden(actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest)) return } allowed, reason, err := a.Authorize(actingAsAttributes) if err != nil || !allowed { glog.V(4).Infof("Forbidden: %#v, Reason: %s, Error: %v", req.RequestURI, reason, err) responsewriters.Forbidden(actingAsAttributes, w, req, reason) return } } newUser := &user.DefaultInfo{ Name: username, Groups: groups, Extra: userExtra, } requestContextMapper.Update(req, request.WithUser(ctx, newUser)) oldUser, _ := request.UserFrom(ctx) httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser) // clear all the impersonation headers from the request req.Header.Del(authenticationapi.ImpersonateUserHeader) req.Header.Del(authenticationapi.ImpersonateGroupHeader) for headerName := range req.Header { if strings.HasPrefix(headerName, authenticationapi.ImpersonateUserExtraHeaderPrefix) { req.Header.Del(headerName) } } handler.ServeHTTP(w, req) }) }
func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { proxyRoundTripper, err := r.getRoundTripper() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if proxyRoundTripper == nil { http.Error(w, "", http.StatusNotFound) return } ctx, ok := r.contextMapper.Get(req) if !ok { http.Error(w, "missing context", http.StatusInternalServerError) return } user, ok := genericapirequest.UserFrom(ctx) if !ok { http.Error(w, "missing user", http.StatusInternalServerError) return } // write a new location based on the existing request pointed at the target service location := &url.URL{} location.Scheme = "https" location.Host = r.getDestinationHost() location.Path = req.URL.Path location.RawQuery = req.URL.Query().Encode() // make a new request object with the updated location and the body we already have newReq, err := http.NewRequest(req.Method, location.String(), req.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } mergeHeader(newReq.Header, req.Header) newReq.ContentLength = req.ContentLength // Copy the TransferEncoding is for future-proofing. Currently Go only supports "chunked" and // it can determine the TransferEncoding based on ContentLength and the Body. newReq.TransferEncoding = req.TransferEncoding upgrade := false // we need to wrap the roundtripper in another roundtripper which will apply the front proxy headers proxyRoundTripper, upgrade, err = r.maybeWrapForConnectionUpgrades(proxyRoundTripper, req) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), proxyRoundTripper) // if we are upgrading, then the upgrade path tries to use this request with the TLS config we provide, but it does // NOT use the roundtripper. Its a direct call that bypasses the round tripper. This means that we have to // attach the "correct" user headers to the request ahead of time. After the initial upgrade, we'll be back // at the roundtripper flow, so we only have to muck with this request, but we do have to do it. if upgrade { transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetGroups(), user.GetExtra()) } handler := genericrest.NewUpgradeAwareProxyHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w}) handler.ServeHTTP(w, newReq) }