mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth Proxy: encoding of non-ASCII headers (#44797)
* Decode auth proxy headers using URL encoding * Header encoding configuration via settings file * Rename configuration setting to headers_encoded * Quoted-printable encoding * Tests for AuthProxy * Fix encoding name * Remove authproxy init
This commit is contained in:
parent
9b5a42845d
commit
1dca39fb91
@ -122,7 +122,7 @@ path = grafana.db
|
|||||||
# For "sqlite3" only. cache mode setting used for connecting to the database
|
# For "sqlite3" only. cache mode setting used for connecting to the database
|
||||||
cache_mode = private
|
cache_mode = private
|
||||||
|
|
||||||
# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
|
# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
|
||||||
locking_attempt_timeout_sec = 0
|
locking_attempt_timeout_sec = 0
|
||||||
|
|
||||||
#################################### Cache server #############################
|
#################################### Cache server #############################
|
||||||
@ -564,6 +564,7 @@ ldap_sync_ttl = 60
|
|||||||
sync_ttl = 60
|
sync_ttl = 60
|
||||||
whitelist =
|
whitelist =
|
||||||
headers =
|
headers =
|
||||||
|
headers_encoded = false
|
||||||
enable_login_token = false
|
enable_login_token = false
|
||||||
|
|
||||||
#################################### Auth JWT ##########################
|
#################################### Auth JWT ##########################
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
|
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
|
||||||
;cache_mode = private
|
;cache_mode = private
|
||||||
|
|
||||||
# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
|
# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0.
|
||||||
;locking_attempt_timeout_sec = 0
|
;locking_attempt_timeout_sec = 0
|
||||||
|
|
||||||
################################### Data sources #########################
|
################################### Data sources #########################
|
||||||
@ -545,6 +545,8 @@
|
|||||||
;sync_ttl = 60
|
;sync_ttl = 60
|
||||||
;whitelist = 192.168.1.1, 192.168.2.1
|
;whitelist = 192.168.1.1, 192.168.2.1
|
||||||
;headers = Email:X-User-Email, Name:X-User-Name
|
;headers = Email:X-User-Email, Name:X-User-Name
|
||||||
|
# Non-ASCII strings in header values are encoded using quoted-printable encoding
|
||||||
|
;headers_encoded = false
|
||||||
# Read the auth proxy docs for details on what the setting below enables
|
# Read the auth proxy docs for details on what the setting below enables
|
||||||
;enable_login_token = false
|
;enable_login_token = false
|
||||||
|
|
||||||
|
@ -32,6 +32,8 @@ whitelist =
|
|||||||
# Optionally define more headers to sync other user attributes
|
# Optionally define more headers to sync other user attributes
|
||||||
# Example `headers = Name:X-WEBAUTH-NAME Role:X-WEBAUTH-ROLE Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS`
|
# Example `headers = Name:X-WEBAUTH-NAME Role:X-WEBAUTH-ROLE Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS`
|
||||||
headers =
|
headers =
|
||||||
|
# Non-ASCII strings in header values are encoded using quoted-printable encoding
|
||||||
|
;headers_encoded = false
|
||||||
# Check out docs on this for more details on the below setting
|
# Check out docs on this for more details on the below setting
|
||||||
enable_login_token = false
|
enable_login_token = false
|
||||||
```
|
```
|
||||||
|
@ -84,14 +84,14 @@ type Options struct {
|
|||||||
|
|
||||||
// New instance of the AuthProxy.
|
// New instance of the AuthProxy.
|
||||||
func New(cfg *setting.Cfg, options *Options) *AuthProxy {
|
func New(cfg *setting.Cfg, options *Options) *AuthProxy {
|
||||||
header := options.Ctx.Req.Header.Get(cfg.AuthProxyHeaderName)
|
auth := &AuthProxy{
|
||||||
return &AuthProxy{
|
|
||||||
remoteCache: options.RemoteCache,
|
remoteCache: options.RemoteCache,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
ctx: options.Ctx,
|
ctx: options.Ctx,
|
||||||
orgID: options.OrgID,
|
orgID: options.OrgID,
|
||||||
header: header,
|
|
||||||
}
|
}
|
||||||
|
auth.header = auth.getDecodedHeader(cfg.AuthProxyHeaderName)
|
||||||
|
return auth
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEnabled checks if the auth proxy is enabled.
|
// IsEnabled checks if the auth proxy is enabled.
|
||||||
@ -313,6 +313,17 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
|
|||||||
return upsert.Result.Id, nil
|
return upsert.Result.Id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDecodedHeader gets decoded value of a header with given headerName
|
||||||
|
func (auth *AuthProxy) getDecodedHeader(headerName string) string {
|
||||||
|
headerValue := auth.ctx.Req.Header.Get(headerName)
|
||||||
|
|
||||||
|
if auth.cfg.AuthProxyHeadersEncoded {
|
||||||
|
headerValue = util.DecodeQuotedPrintable(headerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerValue
|
||||||
|
}
|
||||||
|
|
||||||
// headersIterator iterates over all non-empty supported additional headers
|
// headersIterator iterates over all non-empty supported additional headers
|
||||||
func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
|
func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
|
||||||
for _, field := range supportedHeaderFields {
|
for _, field := range supportedHeaderFields {
|
||||||
@ -321,7 +332,7 @@ func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if value := auth.ctx.Req.Header.Get(h); value != "" {
|
if value := auth.getDecodedHeader(h); value != "" {
|
||||||
fn(field, strings.TrimSpace(value))
|
fn(field, strings.TrimSpace(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,18 +21,19 @@ import (
|
|||||||
|
|
||||||
const hdrName = "markelog"
|
const hdrName = "markelog"
|
||||||
|
|
||||||
func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, cb func(*http.Request, *setting.Cfg)) *AuthProxy {
|
func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, configureReq func(*http.Request, *setting.Cfg)) *AuthProxy {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cfg := setting.NewCfg()
|
|
||||||
cfg.AuthProxyHeaderName = "X-Killa"
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "http://example.com", nil)
|
req, err := http.NewRequest("POST", "http://example.com", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set(cfg.AuthProxyHeaderName, hdrName)
|
|
||||||
|
|
||||||
if cb != nil {
|
cfg := setting.NewCfg()
|
||||||
cb(req, cfg)
|
|
||||||
|
if configureReq != nil {
|
||||||
|
configureReq(req, cfg)
|
||||||
|
} else {
|
||||||
|
cfg.AuthProxyHeaderName = "X-Killa"
|
||||||
|
req.Header.Set(cfg.AuthProxyHeaderName, hdrName)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := &models.ReqContext{
|
ctx := &models.ReqContext{
|
||||||
@ -84,9 +85,11 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
|
auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
|
||||||
|
cfg.AuthProxyHeaderName = "X-Killa"
|
||||||
|
cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS", "Role": "X-WEBAUTH-ROLE"}
|
||||||
|
req.Header.Set(cfg.AuthProxyHeaderName, hdrName)
|
||||||
req.Header.Set("X-WEBAUTH-GROUPS", group)
|
req.Header.Set("X-WEBAUTH-GROUPS", group)
|
||||||
req.Header.Set("X-WEBAUTH-ROLE", role)
|
req.Header.Set("X-WEBAUTH-ROLE", role)
|
||||||
cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS", "Role": "X-WEBAUTH-ROLE"}
|
|
||||||
})
|
})
|
||||||
assert.Equal(t, "auth-proxy-sync-ttl:f5acfffd56daac98d502ef8c8b8c5d56", key)
|
assert.Equal(t, "auth-proxy-sync-ttl:f5acfffd56daac98d502ef8c8b8c5d56", key)
|
||||||
|
|
||||||
@ -191,3 +194,26 @@ func TestMiddlewareContext_ldap(t *testing.T) {
|
|||||||
assert.False(t, stub.LoginCalled)
|
assert.False(t, stub.LoginCalled)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecodeHeader(t *testing.T) {
|
||||||
|
cache := remotecache.NewFakeStore(t)
|
||||||
|
t.Run("should not decode header if not enabled in settings", func(t *testing.T) {
|
||||||
|
auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
|
||||||
|
cfg.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
|
cfg.AuthProxyHeadersEncoded = false
|
||||||
|
req.Header.Set(cfg.AuthProxyHeaderName, "M=C3=BCnchen")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "M=C3=BCnchen", auth.header)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should decode header if enabled in settings", func(t *testing.T) {
|
||||||
|
auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
|
||||||
|
cfg.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
|
cfg.AuthProxyHeadersEncoded = true
|
||||||
|
req.Header.Set(cfg.AuthProxyHeaderName, "M=C3=BCnchen")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "München", auth.header)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -306,6 +306,7 @@ type Cfg struct {
|
|||||||
AuthProxyEnableLoginToken bool
|
AuthProxyEnableLoginToken bool
|
||||||
AuthProxyWhitelist string
|
AuthProxyWhitelist string
|
||||||
AuthProxyHeaders map[string]string
|
AuthProxyHeaders map[string]string
|
||||||
|
AuthProxyHeadersEncoded bool
|
||||||
AuthProxySyncTTL int
|
AuthProxySyncTTL int
|
||||||
|
|
||||||
// OAuth
|
// OAuth
|
||||||
@ -1317,6 +1318,8 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.AuthProxyHeadersEncoded = authProxy.Key("headers_encoded").MustBool(false)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
"mime/quotedprintable"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
@ -71,3 +73,12 @@ func RandomHex(n int) (string, error) {
|
|||||||
}
|
}
|
||||||
return hex.EncodeToString(bytes), nil
|
return hex.EncodeToString(bytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeQuotedPrintable decodes quoted-printable UTF-8 string
|
||||||
|
func DecodeQuotedPrintable(encodedValue string) string {
|
||||||
|
decodedBytes, err := io.ReadAll(quotedprintable.NewReader(strings.NewReader(encodedValue)))
|
||||||
|
if err != nil {
|
||||||
|
return encodedValue
|
||||||
|
}
|
||||||
|
return string(decodedBytes)
|
||||||
|
}
|
||||||
|
@ -32,3 +32,73 @@ func TestEncodePassword(t *testing.T) {
|
|||||||
encodedPassword,
|
encodedPassword,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecodeQuotedPrintable(t *testing.T) {
|
||||||
|
t.Run("should return not encoded string as is", func(t *testing.T) {
|
||||||
|
testStrings := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{" ", ""},
|
||||||
|
{"munich", "munich"},
|
||||||
|
{" munich", " munich"},
|
||||||
|
{"munich gothenburg", "munich gothenburg"},
|
||||||
|
{"München", "München"},
|
||||||
|
{"München Göteborg", "München Göteborg"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, str := range testStrings {
|
||||||
|
val := DecodeQuotedPrintable(str.in)
|
||||||
|
assert.Equal(t, str.out, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should decode encoded string", func(t *testing.T) {
|
||||||
|
testStrings := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"M=C3=BCnchen", "München"},
|
||||||
|
{"M=C3=BCnchen G=C3=B6teborg", "München Göteborg"},
|
||||||
|
{"=E5=85=AC=E5=8F=B8", "公司"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, str := range testStrings {
|
||||||
|
val := DecodeQuotedPrintable(str.in)
|
||||||
|
assert.Equal(t, str.out, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should gracefully ignore invalid encoding sequences", func(t *testing.T) {
|
||||||
|
testStrings := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"=XY=ZZ", "=XY=ZZ"},
|
||||||
|
{"==58", "=X"},
|
||||||
|
{"munich = gothenburg", "munich = gothenburg"},
|
||||||
|
{"munich == tromso", "munich == tromso"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, str := range testStrings {
|
||||||
|
val := DecodeQuotedPrintable(str.in)
|
||||||
|
assert.Equal(t, str.out, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return invalid UTF-8 sequences as is", func(t *testing.T) {
|
||||||
|
testStrings := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"=E5 =85=AC =E5=8F =B8", "\xE5 \x85\xAC \xE5\x8F \xB8"},
|
||||||
|
{"=00=00munich=FF=FF", "\x00\x00munich\xFF\xFF"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, str := range testStrings {
|
||||||
|
val := DecodeQuotedPrintable(str.in)
|
||||||
|
assert.Equal(t, str.out, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user