AuthProxy: Allow disabling Auth Proxy cache (#83755)

* extract auth proxy settings

* simplify auth proxy methods

* add doc mentions
This commit is contained in:
Jo
2024-03-01 11:31:06 +01:00
committed by GitHub
parent 1cec975a66
commit 36a19bfa83
23 changed files with 145 additions and 110 deletions

View File

@@ -122,7 +122,7 @@ func ProvideService(
}
}
if s.cfg.AuthProxyEnabled && len(proxyClients) > 0 {
if s.cfg.AuthProxy.Enabled && len(proxyClients) > 0 {
proxy, err := clients.ProvideProxy(cfg, cache, proxyClients...)
if err != nil {
s.log.Error("Failed to configure auth proxy", "err", err)

View File

@@ -13,7 +13,7 @@ func (s *Service) getUsageStats(ctx context.Context) (map[string]any, error) {
authTypes := map[string]bool{}
authTypes["basic_auth"] = s.cfg.BasicAuthEnabled
authTypes["ldap"] = s.cfg.LDAPAuthEnabled
authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled
authTypes["auth_proxy"] = s.cfg.AuthProxy.Enabled
authTypes["anonymous"] = s.cfg.AnonymousEnabled
authTypes["jwt"] = s.cfg.JWTAuth.Enabled
authTypes["grafana_password"] = !s.cfg.DisableLogin

View File

@@ -20,7 +20,7 @@ func TestService_getUsageStats(t *testing.T) {
svc.cfg.DisableLoginForm = false
svc.cfg.DisableLogin = false
svc.cfg.BasicAuthEnabled = true
svc.cfg.AuthProxyEnabled = true
svc.cfg.AuthProxy.Enabled = true
svc.cfg.JWTAuth.Enabled = true
svc.cfg.LDAPAuthEnabled = true
svc.cfg.EditorsCanAdmin = true

View File

@@ -40,11 +40,11 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern
FetchSyncedUser: true,
SyncOrgRoles: true,
SyncPermissions: true,
AllowSignUp: c.cfg.AuthProxyAutoSignUp,
AllowSignUp: c.cfg.AuthProxy.AutoSignUp,
},
}
switch c.cfg.AuthProxyHeaderProperty {
switch c.cfg.AuthProxy.HeaderProperty {
case "username":
identity.Login = username
addr, err := mail.ParseAddress(username)
@@ -55,7 +55,7 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern
identity.Login = username
identity.Email = username
default:
return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxyHeaderProperty)
return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxy.HeaderProperty)
}
if v, ok := additional[proxyFieldName]; ok {

View File

@@ -94,8 +94,8 @@ func TestGrafana_AuthenticateProxy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxyAutoSignUp = true
cfg.AuthProxyHeaderProperty = tt.proxyProperty
cfg.AuthProxy.AutoSignUp = true
cfg.AuthProxy.HeaderProperty = tt.proxyProperty
c := ProvideGrafana(cfg, usertest.NewUserServiceFake())
identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional)

View File

@@ -3,6 +3,7 @@ package clients
import (
"context"
"encoding/hex"
"errors"
"fmt"
"hash/fnv"
"net"
@@ -12,6 +13,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
authidentity "github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
@@ -43,7 +45,7 @@ var (
)
func ProvideProxy(cfg *setting.Cfg, cache proxyCache, clients ...authn.ProxyClient) (*Proxy, error) {
list, err := parseAcceptList(cfg.AuthProxyWhitelist)
list, err := parseAcceptList(cfg.AuthProxy.Whitelist)
if err != nil {
return nil, err
}
@@ -73,34 +75,22 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list")
}
username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)
username := getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded)
if len(username) == 0 {
return nil, errEmptyProxyHeader.Errorf("no username provided in auth proxy header")
}
additional := getAdditionalProxyHeaders(r, c.cfg)
cacheKey, ok := getProxyCacheKey(username, additional)
if ok {
// See if we have cached the user id, in that case we can fetch the signed-in user and skip sync.
// Error here means that we could not find anything in cache, so we can proceed as usual
if entry, err := c.cache.Get(ctx, cacheKey); err == nil {
uid, err := strconv.ParseInt(string(entry), 10, 64)
if err != nil {
c.log.FromContext(ctx).Warn("Failed to parse user id from cache", "error", err, "userId", string(entry))
} else {
return &authn.Identity{
ID: authn.NamespacedID(authn.NamespaceUser, uid),
OrgID: r.OrgID,
// FIXME: This does not match the actual auth module used, but should not have any impact
// Maybe caching the auth module used with the user ID would be a good idea
AuthenticatedBy: login.AuthProxyAuthModule,
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
},
}, nil
}
if c.cfg.AuthProxy.SyncTTL != 0 && ok {
identity, errCache := c.retrieveIDFromCache(ctx, cacheKey, r)
if errCache == nil {
return identity, nil
}
if !errors.Is(errCache, remotecache.ErrCacheItemNotFound) {
c.log.FromContext(ctx).Warn("Failed to fetch auth proxy info from cache", "error", errCache)
}
}
@@ -117,8 +107,34 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
return nil, clientErr
}
// See if we have cached the user id, in that case we can fetch the signed-in user and skip sync.
// Error here means that we could not find anything in cache, so we can proceed as usual
func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *authn.Request) (*authn.Identity, error) {
entry, err := c.cache.Get(ctx, cacheKey)
if err != nil {
return nil, err
}
uid, err := strconv.ParseInt(string(entry), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse user id from cache: %w - entry: %s", err, string(entry))
}
return &authn.Identity{
ID: authn.NamespacedID(authn.NamespaceUser, uid),
OrgID: r.OrgID,
// FIXME: This does not match the actual auth module used, but should not have any impact
// Maybe caching the auth module used with the user ID would be a good idea
AuthenticatedBy: login.AuthProxyAuthModule,
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
},
}, nil
}
func (c *Proxy) Test(ctx context.Context, r *authn.Request) bool {
return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0
return len(getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded)) != 0
}
func (c *Proxy) Priority() uint {
@@ -147,7 +163,7 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req
// 3. Name = x; Role = Admin # cache hit with key Name=x;Role=Admin, no update, the user stays with Role=Editor
// To avoid such a problem we also cache the key used using `prefix:[username]`.
// Then whenever we get a cache miss due to changes in any header we use it to invalidate the previous item.
username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)
username := getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded)
userKey := fmt.Sprintf("%s:%s", proxyCachePrefix, username)
// invalidate previously cached user id
@@ -159,7 +175,7 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req
c.log.FromContext(ctx).Debug("Cache proxy user", "userId", id)
bytes := []byte(strconv.FormatInt(id, 10))
duration := time.Duration(c.cfg.AuthProxySyncTTL) * time.Minute
duration := time.Duration(c.cfg.AuthProxy.SyncTTL) * time.Minute
if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, duration); err != nil {
c.log.Warn("Failed to cache proxy user", "error", err, "userId", id)
}
@@ -232,7 +248,7 @@ func getProxyHeader(r *authn.Request, headerName string, encoded bool) string {
func getAdditionalProxyHeaders(r *authn.Request, cfg *setting.Cfg) map[string]string {
additional := make(map[string]string, len(proxyFields))
for _, k := range proxyFields {
if v := getProxyHeader(r, cfg.AuthProxyHeaders[k], cfg.AuthProxyHeadersEncoded); v != "" {
if v := getProxyHeader(r, cfg.AuthProxy.Headers[k], cfg.AuthProxy.HeadersEncoded); v != "" {
additional[k] = v
}
}

View File

@@ -100,9 +100,9 @@ func TestProxy_Authenticate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxyHeaderName = "X-Username"
cfg.AuthProxyHeaders = tt.proxyHeaders
cfg.AuthProxyWhitelist = tt.ips
cfg.AuthProxy.HeaderName = "X-Username"
cfg.AuthProxy.Headers = tt.proxyHeaders
cfg.AuthProxy.Whitelist = tt.ips
calledUsername := ""
var calledAdditional map[string]string
@@ -166,7 +166,7 @@ func TestProxy_Test(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxyHeaderName = "Proxy-Header"
cfg.AuthProxy.HeaderName = "Proxy-Header"
c, _ := ProvideProxy(cfg, nil, nil, nil)
assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req))
@@ -197,8 +197,8 @@ func (f fakeCache) Delete(ctx context.Context, key string) error {
func TestProxy_Hook(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxyHeaderName = "X-Username"
cfg.AuthProxyHeaders = map[string]string{
cfg.AuthProxy.HeaderName = "X-Username"
cfg.AuthProxy.Headers = map[string]string{
proxyFieldRole: "X-Role",
}
cache := &fakeCache{data: make(map[string][]byte)}