diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 2a1c1c559f3..03fdee8bf2e 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -496,6 +496,8 @@ report-dir report-prefix requestheader-allowed-names requestheader-client-ca-file +requestheader-extra-headers-prefix +requestheader-group-headers requestheader-username-headers require-kubeconfig required-contexts diff --git a/pkg/apiserver/authenticator/builtin.go b/pkg/apiserver/authenticator/builtin.go index 65280ae677e..f4e88101052 100644 --- a/pkg/apiserver/authenticator/builtin.go +++ b/pkg/apiserver/authenticator/builtin.go @@ -43,6 +43,11 @@ import ( type RequestHeaderConfig struct { // UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins. UsernameHeaders []string + // GroupHeaders are the headers to check (case-insensitively) for a group names. All values will be used. + GroupHeaders []string + // ExtraHeaderPrefixes are the head prefixes to check (case-insentively) for filling in + // the user.Info.Extra. All values of all matching headers will be added. + ExtraHeaderPrefixes []string // ClientCA points to CA bundle file which is used verify the identity of the front proxy ClientCA string // AllowedClientNames is a list of common names that may be presented by the authenticating front proxy. Empty means: accept any. @@ -88,6 +93,8 @@ func New(config AuthenticatorConfig) (authenticator.Request, *spec.SecurityDefin config.RequestHeaderConfig.ClientCA, config.RequestHeaderConfig.AllowedClientNames, config.RequestHeaderConfig.UsernameHeaders, + config.RequestHeaderConfig.GroupHeaders, + config.RequestHeaderConfig.ExtraHeaderPrefixes, ) if err != nil { return nil, nil, err diff --git a/pkg/genericapiserver/options/authentication.go b/pkg/genericapiserver/options/authentication.go index ecf09887626..fd76c1a3286 100644 --- a/pkg/genericapiserver/options/authentication.go +++ b/pkg/genericapiserver/options/authentication.go @@ -63,12 +63,6 @@ type PasswordFileAuthenticationOptions struct { BasicAuthFile string } -type RequestHeaderAuthenticationOptions struct { - UsernameHeaders []string - ClientCAFile string - AllowedNames []string -} - type ServiceAccountAuthenticationOptions struct { KeyFiles []string Lookup bool @@ -206,17 +200,7 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { } if s.RequestHeader != nil { - fs.StringSliceVar(&s.RequestHeader.UsernameHeaders, "requestheader-username-headers", s.RequestHeader.UsernameHeaders, ""+ - "List of request headers to inspect for usernames. X-Remote-User is common.") - - fs.StringVar(&s.RequestHeader.ClientCAFile, "requestheader-client-ca-file", s.RequestHeader.ClientCAFile, ""+ - "Root certificate bundle to use to verify client certificates on incoming requests "+ - "before trusting usernames in headers specified by --requestheader-username-headers") - - fs.StringSliceVar(&s.RequestHeader.AllowedNames, "requestheader-allowed-names", s.RequestHeader.AllowedNames, ""+ - "List of client certificate common names to allow to provide usernames in headers "+ - "specified by --requestheader-username-headers. If empty, any client certificate validated "+ - "by the authorities in --requestheader-client-ca-file is allowed.") + s.RequestHeader.AddFlags(fs) } if s.ServiceAccounts != nil { @@ -275,7 +259,7 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig(clientCAFile strin } if s.RequestHeader != nil { - ret.RequestHeaderConfig = s.RequestHeader.AuthenticationRequestHeaderConfig() + ret.RequestHeaderConfig = s.RequestHeader.ToAuthenticationRequestHeaderConfig() } if s.ServiceAccounts != nil { @@ -295,17 +279,47 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig(clientCAFile strin return ret } -// AuthenticationRequestHeaderConfig returns an authenticator config object for these options -// if necessary. nil otherwise. -func (s *RequestHeaderAuthenticationOptions) AuthenticationRequestHeaderConfig() *authenticator.RequestHeaderConfig { +type RequestHeaderAuthenticationOptions struct { + UsernameHeaders []string + GroupHeaders []string + ExtraHeaderPrefixes []string + ClientCAFile string + AllowedNames []string +} + +func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringSliceVar(&s.UsernameHeaders, "requestheader-username-headers", s.UsernameHeaders, ""+ + "List of request headers to inspect for usernames. X-Remote-User is common.") + + fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+ + "List of request headers to inspect for groups. X-Remote-Group is suggested.") + + fs.StringSliceVar(&s.ExtraHeaderPrefixes, "requestheader-extra-headers-prefix", s.ExtraHeaderPrefixes, ""+ + "List of request header prefixes to inspect. X-Remote-Extra- is suggested.") + + fs.StringVar(&s.ClientCAFile, "requestheader-client-ca-file", s.ClientCAFile, ""+ + "Root certificate bundle to use to verify client certificates on incoming requests "+ + "before trusting usernames in headers specified by --requestheader-username-headers") + + fs.StringSliceVar(&s.AllowedNames, "requestheader-allowed-names", s.AllowedNames, ""+ + "List of client certificate common names to allow to provide usernames in headers "+ + "specified by --requestheader-username-headers. If empty, any client certificate validated "+ + "by the authorities in --requestheader-client-ca-file is allowed.") +} + +// ToAuthenticationRequestHeaderConfig returns a RequestHeaderConfig config object for these options +// if necessary, nil otherwise. +func (s *RequestHeaderAuthenticationOptions) ToAuthenticationRequestHeaderConfig() *authenticator.RequestHeaderConfig { if len(s.UsernameHeaders) == 0 { return nil } return &authenticator.RequestHeaderConfig{ - UsernameHeaders: s.UsernameHeaders, - ClientCA: s.ClientCAFile, - AllowedClientNames: s.AllowedNames, + UsernameHeaders: s.UsernameHeaders, + GroupHeaders: s.GroupHeaders, + ExtraHeaderPrefixes: s.ExtraHeaderPrefixes, + ClientCA: s.ClientCAFile, + AllowedClientNames: s.AllowedNames, } } diff --git a/plugin/pkg/auth/authenticator/request/headerrequest/requestheader.go b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader.go index cd704fbdfe0..86fe515ea72 100644 --- a/plugin/pkg/auth/authenticator/request/headerrequest/requestheader.go +++ b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader.go @@ -33,23 +33,51 @@ import ( type requestHeaderAuthRequestHandler struct { // nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins. nameHeaders []string + + // groupHeaders are the headers to check (case-insensitively) for group membership. All values of all headers will be added. + groupHeaders []string + + // extraHeaderPrefixes are the head prefixes to check (case-insensitively) for filling in + // the user.Info.Extra. All values of all matching headers will be added. + extraHeaderPrefixes []string } -func New(nameHeaders []string) (authenticator.Request, error) { - headers := []string{} - for _, headerName := range nameHeaders { +func New(nameHeaders []string, groupHeaders []string, extraHeaderPrefixes []string) (authenticator.Request, error) { + trimmedNameHeaders, err := trimHeaders(nameHeaders...) + if err != nil { + return nil, err + } + trimmedGroupHeaders, err := trimHeaders(groupHeaders...) + if err != nil { + return nil, err + } + trimmedExtraHeaderPrefixes, err := trimHeaders(extraHeaderPrefixes...) + if err != nil { + return nil, err + } + + return &requestHeaderAuthRequestHandler{ + nameHeaders: trimmedNameHeaders, + groupHeaders: trimmedGroupHeaders, + extraHeaderPrefixes: trimmedExtraHeaderPrefixes, + }, nil +} + +func trimHeaders(headerNames ...string) ([]string, error) { + ret := []string{} + for _, headerName := range headerNames { trimmedHeader := strings.TrimSpace(headerName) if len(trimmedHeader) == 0 { return nil, fmt.Errorf("empty header %q", headerName) } - headers = append(headers, trimmedHeader) + ret = append(ret, trimmedHeader) } - return &requestHeaderAuthRequestHandler{nameHeaders: headers}, nil + return ret, nil } -func NewSecure(clientCA string, proxyClientNames []string, nameHeaders []string) (authenticator.Request, error) { - headerAuthenticator, err := New(nameHeaders) +func NewSecure(clientCA string, proxyClientNames []string, nameHeaders []string, groupHeaders []string, extraHeaderPrefixes []string) (authenticator.Request, error) { + headerAuthenticator, err := New(nameHeaders, groupHeaders, extraHeaderPrefixes) if err != nil { return nil, err } @@ -81,8 +109,27 @@ func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) if len(name) == 0 { return nil, false, nil } + groups := allHeaderValues(req.Header, a.groupHeaders) + extra := newExtra(req.Header, a.extraHeaderPrefixes) - return &user.DefaultInfo{Name: name}, true, nil + // clear headers used for authentication + for _, headerName := range a.nameHeaders { + req.Header.Del(headerName) + } + for _, headerName := range a.groupHeaders { + req.Header.Del(headerName) + } + for k := range extra { + for _, prefix := range a.extraHeaderPrefixes { + req.Header.Del(prefix + k) + } + } + + return &user.DefaultInfo{ + Name: name, + Groups: groups, + Extra: extra, + }, true, nil } func headerValue(h http.Header, headerNames []string) string { @@ -94,3 +141,38 @@ func headerValue(h http.Header, headerNames []string) string { } return "" } + +func allHeaderValues(h http.Header, headerNames []string) []string { + ret := []string{} + for _, headerName := range headerNames { + values, ok := h[headerName] + if !ok { + continue + } + + for _, headerValue := range values { + if len(headerValue) > 0 { + ret = append(ret, headerValue) + } + } + } + return ret +} + +func newExtra(h http.Header, headerPrefixes []string) map[string][]string { + ret := map[string][]string{} + + // we have to iterate over prefixes first in order to have proper ordering inside the value slices + for _, prefix := range headerPrefixes { + for headerName, vv := range h { + if !strings.HasPrefix(strings.ToLower(headerName), strings.ToLower(prefix)) { + continue + } + + extraKey := strings.ToLower(headerName[len(prefix):]) + ret[extraKey] = append(ret[extraKey], vv...) + } + } + + return ret +} diff --git a/plugin/pkg/auth/authenticator/request/headerrequest/requestheader_test.go b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader_test.go index 75402ab64e5..b7009d57345 100644 --- a/plugin/pkg/auth/authenticator/request/headerrequest/requestheader_test.go +++ b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader_test.go @@ -26,30 +26,36 @@ import ( func TestRequestHeader(t *testing.T) { testcases := map[string]struct { - nameHeaders []string - requestHeaders http.Header + nameHeaders []string + groupHeaders []string + extraPrefixHeaders []string + requestHeaders http.Header expectedUser user.Info expectedOk bool }{ "empty": {}, - "no match": { + "user no match": { nameHeaders: []string{"X-Remote-User"}, }, - "match": { + "user match": { nameHeaders: []string{"X-Remote-User"}, requestHeaders: http.Header{"X-Remote-User": {"Bob"}}, - expectedUser: &user.DefaultInfo{Name: "Bob"}, - expectedOk: true, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedOk: true, }, - "exact match": { + "user exact match": { nameHeaders: []string{"X-Remote-User"}, requestHeaders: http.Header{ "Prefixed-X-Remote-User-With-Suffix": {"Bob"}, "X-Remote-User-With-Suffix": {"Bob"}, }, }, - "first match": { + "user first match": { nameHeaders: []string{ "X-Remote-User", "A-Second-X-Remote-User", @@ -59,19 +65,83 @@ func TestRequestHeader(t *testing.T) { "X-Remote-User": {"", "First header, second value"}, "A-Second-X-Remote-User": {"Second header, first value", "Second header, second value"}, "Another-X-Remote-User": {"Third header, first value"}}, - expectedUser: &user.DefaultInfo{Name: "Second header, first value"}, - expectedOk: true, + expectedUser: &user.DefaultInfo{ + Name: "Second header, first value", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedOk: true, }, - "case-insensitive": { + "user case-insensitive": { nameHeaders: []string{"x-REMOTE-user"}, // configured headers can be case-insensitive requestHeaders: http.Header{"X-Remote-User": {"Bob"}}, // the parsed headers are normalized by the http package - expectedUser: &user.DefaultInfo{Name: "Bob"}, - expectedOk: true, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedOk: true, + }, + + "groups none": { + nameHeaders: []string{"X-Remote-User"}, + groupHeaders: []string{"X-Remote-Group"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Bob"}, + }, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedOk: true, + }, + "groups all matches": { + nameHeaders: []string{"X-Remote-User"}, + groupHeaders: []string{"X-Remote-Group-1", "X-Remote-Group-2"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Bob"}, + "X-Remote-Group-1": {"one-a", "one-b"}, + "X-Remote-Group-2": {"two-a", "two-b"}, + }, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + Groups: []string{"one-a", "one-b", "two-a", "two-b"}, + Extra: map[string][]string{}, + }, + expectedOk: true, + }, + + "extra prefix matches case-insensitive": { + nameHeaders: []string{"X-Remote-User"}, + groupHeaders: []string{"X-Remote-Group-1", "X-Remote-Group-2"}, + extraPrefixHeaders: []string{"X-Remote-Extra-1-", "X-Remote-Extra-2-"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Bob"}, + "X-Remote-Group-1": {"one-a", "one-b"}, + "X-Remote-Group-2": {"two-a", "two-b"}, + "X-Remote-extra-1-key1": {"alfa", "bravo"}, + "X-Remote-Extra-1-Key2": {"charlie", "delta"}, + "X-Remote-Extra-1-": {"india", "juliet"}, + "X-Remote-extra-2-": {"kilo", "lima"}, + "X-Remote-extra-2-Key1": {"echo", "foxtrot"}, + "X-Remote-Extra-2-key2": {"golf", "hotel"}, + }, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + Groups: []string{"one-a", "one-b", "two-a", "two-b"}, + Extra: map[string][]string{ + "key1": {"alfa", "bravo", "echo", "foxtrot"}, + "key2": {"charlie", "delta", "golf", "hotel"}, + "": {"india", "juliet", "kilo", "lima"}, + }, + }, + expectedOk: true, }, } for k, testcase := range testcases { - auth, err := New(testcase.nameHeaders) + auth, err := New(testcase.nameHeaders, testcase.groupHeaders, testcase.extraPrefixHeaders) if err != nil { t.Fatal(err) }