Merge pull request #65799 from dekkagaijin/fix-headers
Automatic merge from submit-queue (batch tested with PRs 66225, 66648, 65799, 66630, 66619). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Percent-encode illegal characters in user.Info.Extra keys This percent-encodes characters in `X-Remote-Extra-` and `Impersonate-Extra-` keys which aren't valid for header names per [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2.6) (plus "%" to avoid breaking keys which contain them). The API server then blindly unescapes these keys. Reviewer note: Old clients sending keys which were `%`-escaped by the user will have their values unescaped by new API servers. New clients sending keys containing illegal characters (or "%") to old API servers will not have their values unescaped. This version skew incompatibility is a compromise discussed in #63682. Fixes #63682 PTAL @mikedanese **Release note**: ```release-note action required: the API server and client-go libraries have been fixed to support additional non-alpha-numeric characters in UserInfo "extra" data keys. Both should be updated in order to properly support extra data containing "/" characters or other characters disallowed in HTTP headers. ``` Kubernetes-commit: 6715f139292bfde5e4030e2e3f8077da04cc6d72
This commit is contained in:
commit
e16db054ca
File diff suppressed because it is too large
Load Diff
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
|
@ -160,6 +161,14 @@ func allHeaderValues(h http.Header, headerNames []string) []string {
|
|||
return ret
|
||||
}
|
||||
|
||||
func unescapeExtraKey(encodedKey string) string {
|
||||
key, err := url.PathUnescape(encodedKey) // Decode %-encoded bytes.
|
||||
if err != nil {
|
||||
return encodedKey // Always record extra strings, even if malformed/unencoded.
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func newExtra(h http.Header, headerPrefixes []string) map[string][]string {
|
||||
ret := map[string][]string{}
|
||||
|
||||
|
|
@ -170,7 +179,7 @@ func newExtra(h http.Header, headerPrefixes []string) map[string][]string {
|
|||
continue
|
||||
}
|
||||
|
||||
extraKey := strings.ToLower(headerName[len(prefix):])
|
||||
extraKey := unescapeExtraKey(strings.ToLower(headerName[len(prefix):]))
|
||||
ret[extraKey] = append(ret[extraKey], vv...)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,37 @@ func TestRequestHeader(t *testing.T) {
|
|||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
|
||||
"escaped extra keys": {
|
||||
nameHeaders: []string{"X-Remote-User"},
|
||||
groupHeaders: []string{"X-Remote-Group"},
|
||||
extraPrefixHeaders: []string{"X-Remote-Extra-"},
|
||||
requestHeaders: http.Header{
|
||||
"X-Remote-User": {"Bob"},
|
||||
"X-Remote-Group": {"one-a", "one-b"},
|
||||
"X-Remote-Extra-Alpha": {"alphabetical"},
|
||||
"X-Remote-Extra-Alph4num3r1c": {"alphanumeric"},
|
||||
"X-Remote-Extra-Percent%20encoded": {"percent encoded"},
|
||||
"X-Remote-Extra-Almost%zzpercent%xxencoded": {"not quite percent encoded"},
|
||||
"X-Remote-Extra-Example.com%2fpercent%2520encoded": {"url with double percent encoding"},
|
||||
"X-Remote-Extra-Example.com%2F%E4%BB%8A%E6%97%A5%E3%81%AF": {"url with unicode"},
|
||||
"X-Remote-Extra-Abc123!#$+.-_*\\^`~|'": {"header key legal characters"},
|
||||
},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "Bob",
|
||||
Groups: []string{"one-a", "one-b"},
|
||||
Extra: map[string][]string{
|
||||
"alpha": {"alphabetical"},
|
||||
"alph4num3r1c": {"alphanumeric"},
|
||||
"percent encoded": {"percent encoded"},
|
||||
"almost%zzpercent%xxencoded": {"not quite percent encoded"},
|
||||
"example.com/percent%20encoded": {"url with double percent encoding"},
|
||||
"example.com/今日は": {"url with unicode"},
|
||||
"abc123!#$+.-_*\\^`~|'": {"header key legal characters"},
|
||||
},
|
||||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k, testcase := range testcases {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
|
@ -146,6 +147,14 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
|
|||
})
|
||||
}
|
||||
|
||||
func unescapeExtraKey(encodedKey string) string {
|
||||
key, err := url.PathUnescape(encodedKey) // Decode %-encoded bytes.
|
||||
if err != nil {
|
||||
return encodedKey // Always record extra strings, even if malformed/unencoded.
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// buildImpersonationRequests returns a list of objectreferences that represent the different things we're requesting to impersonate.
|
||||
// Also includes a map[string][]string representing user.Info.Extra
|
||||
// Each request must be authorized against the current user before switching contexts.
|
||||
|
|
@ -175,7 +184,7 @@ func buildImpersonationRequests(headers http.Header) ([]v1.ObjectReference, erro
|
|||
}
|
||||
|
||||
hasUserExtra = true
|
||||
extraKey := strings.ToLower(headerName[len(authenticationv1.ImpersonateUserExtraHeaderPrefix):])
|
||||
extraKey := unescapeExtraKey(strings.ToLower(headerName[len(authenticationv1.ImpersonateUserExtraHeaderPrefix):]))
|
||||
|
||||
// make a separate request for each extra value they're trying to set
|
||||
for _, value := range values {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (authorized auth
|
|||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 1 && (user.GetGroups()[1] == "escaped-scopes" || user.GetGroups()[1] == "almost-escaped-scopes") {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-particular-scopes" &&
|
||||
a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetName() == "scope-a" {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
|
|
@ -224,6 +228,36 @@ func TestImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "percent-escaped-userextras",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "escaped-scopes"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationUserExtras: map[string][]string{"example.com%2fescaped%e1%9b%84scopes": {"scope-a", "scope-b"}},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "system:admin",
|
||||
Groups: []string{"system:authenticated"},
|
||||
Extra: map[string][]string{"example.com/escapedᛄscopes": {"scope-a", "scope-b"}},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "almost-percent-escaped-userextras",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "almost-escaped-scopes"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationUserExtras: map[string][]string{"almost%zzpercent%xxencoded": {"scope-a", "scope-b"}},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "system:admin",
|
||||
Groups: []string{"system:authenticated"},
|
||||
Extra: map[string][]string{"almost%zzpercent%xxencoded": {"scope-a", "scope-b"}},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "allowed-users-impersonation",
|
||||
user: &user.DefaultInfo{
|
||||
|
|
|
|||
Loading…
Reference in New Issue