grafana/pkg/services/authn/clients/proxy_test.go
Karl Persson 8bcd9c2594
Identity: Remove typed id (#91801)
* Refactor identity struct to store type in separate field

* Update ResolveIdentity to take string representation of typedID

* Add IsIdentityType to requester interface

* Use IsIdentityType from interface

* Remove usage of TypedID

* Remote typedID struct

* fix GetInternalID
2024-08-13 10:18:28 +02:00

242 lines
6.3 KiB
Go

package clients
import (
"context"
"errors"
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/setting"
)
func TestProxy_Authenticate(t *testing.T) {
type testCase struct {
desc string
req *authn.Request
ips string
proxyHeader string
proxyHeaders map[string]string
expectedErr error
expectedUsername string
expectedAdditional map[string]string
}
tests := []testCase{
{
desc: "should authenticate using passed in proxy client",
ips: "127.0.0.1",
req: &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{
"X-Username": {"username"},
"X-Name": {"name"},
"X-Email": {"email"},
"X-Login": {"login"},
"X-Role": {"Viewer"},
"X-Group": {"grp1,grp2"},
},
RemoteAddr: "127.0.0.1:333",
},
},
proxyHeader: "X-Username",
proxyHeaders: map[string]string{
proxyFieldName: "X-Name",
proxyFieldEmail: "X-Email",
proxyFieldLogin: "X-Login",
proxyFieldRole: "X-Role",
proxyFieldGroups: "X-Group",
},
expectedUsername: "username",
expectedAdditional: map[string]string{
proxyFieldName: "name",
proxyFieldEmail: "email",
proxyFieldLogin: "login",
proxyFieldRole: "Viewer",
proxyFieldGroups: "grp1,grp2",
},
},
{
desc: "should fail when proxy header is empty",
req: &authn.Request{
HTTPRequest: &http.Request{Header: map[string][]string{
"X-Username": {""},
"X-Name": {"name"},
"X-Email": {"email"},
"X-Login": {"login"},
"X-Role": {"Viewer"},
"X-Group": {"grp1,grp2"},
}},
},
proxyHeader: "X-Username",
proxyHeaders: map[string]string{
proxyFieldName: "X-Name",
proxyFieldEmail: "X-Email",
proxyFieldLogin: "X-Login",
proxyFieldRole: "X-Role",
proxyFieldGroups: "X-Group",
},
expectedErr: errEmptyProxyHeader,
},
{
desc: "should fail when caller ip is not in accept list",
req: &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{},
RemoteAddr: "127.0.0.2:333",
},
},
ips: "127.0.0.1",
expectedErr: errNotAcceptedIP,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxy.HeaderName = "X-Username"
cfg.AuthProxy.Headers = tt.proxyHeaders
cfg.AuthProxy.Whitelist = tt.ips
calledUsername := ""
var calledAdditional map[string]string
proxyClient := authntest.MockProxyClient{AuthenticateProxyFunc: func(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) {
calledUsername = username
calledAdditional = additional
return nil, nil
}}
c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, proxyClient)
require.NoError(t, err)
_, err = c.Authenticate(context.Background(), tt.req)
assert.ErrorIs(t, err, tt.expectedErr)
assert.Equal(t, tt.expectedUsername, calledUsername)
assert.EqualValues(t, tt.expectedAdditional, calledAdditional)
})
}
}
func TestProxy_Test(t *testing.T) {
type testCase struct {
desc string
req *authn.Request
expectedOK bool
}
tests := []testCase{
{
desc: "should return true when proxy header exists",
req: &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{"Proxy-Header": {"some value"}},
},
},
expectedOK: true,
},
{
desc: "should return false when proxy header exists but has no value",
req: &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{"Proxy-Header": {""}},
},
},
expectedOK: false,
},
{
desc: "should return false when no proxy header is set on request",
req: &authn.Request{
HTTPRequest: &http.Request{Header: map[string][]string{}},
},
expectedOK: false,
},
{
desc: "should return false when no http request is present",
req: &authn.Request{},
expectedOK: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxy.HeaderName = "Proxy-Header"
c, _ := ProvideProxy(cfg, nil, nil, nil)
assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req))
})
}
}
var _ proxyCache = new(fakeCache)
type fakeCache struct {
data map[string][]byte
expectedErr error
}
func (f *fakeCache) Get(ctx context.Context, key string) ([]byte, error) {
return f.data[key], f.expectedErr
}
func (f *fakeCache) Set(ctx context.Context, key string, value []byte, expire time.Duration) error {
f.data[key] = value
return f.expectedErr
}
func (f fakeCache) Delete(ctx context.Context, key string) error {
delete(f.data, key)
return f.expectedErr
}
func TestProxy_Hook(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxy.HeaderName = "X-Username"
cfg.AuthProxy.Headers = map[string]string{
proxyFieldRole: "X-Role",
}
cache := &fakeCache{data: make(map[string][]byte)}
// withRole creates a test case for a user with a specific role.
withRole := func(role string) func(t *testing.T) {
cacheKey := fmt.Sprintf("users:johndoe-%s", role)
return func(t *testing.T) {
c, err := ProvideProxy(cfg, cache, authntest.MockProxyClient{})
require.NoError(t, err)
userIdentity := &authn.Identity{
ID: "1",
Type: claims.TypeUser,
ClientParams: authn.ClientParams{
CacheAuthProxyKey: cacheKey,
},
}
userReq := &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{
"X-Username": {"johndoe"},
"X-Role": {role},
},
},
}
err = c.Hook(context.Background(), userIdentity, userReq)
assert.NoError(t, err)
expectedCache := map[string][]byte{
cacheKey: []byte("1"),
fmt.Sprintf("%s:%s", proxyCachePrefix, "johndoe"): []byte(fmt.Sprintf("users:johndoe-%s", role)),
}
assert.Equal(t, expectedCache, cache.data)
}
}
t.Run("step 1: new user with role Admin", withRole("Admin"))
t.Run("step 2: cached user with new Role Viewer", withRole("Viewer"))
t.Run("step 3: cached user get changed back to Admin", withRole("Admin"))
}