AuthN: Add auth proxy client (#61555)

* AuthN: set up boilerplate for proxy client

* AuthN: Implement Test for proxy client

* AuthN: parse accept list in constructor

* AuthN: add proxy client interface

* AuthN: handle error

* AuthN: Implement the proxy client interface for ldap

* AuthN: change reciever name

* AuthN: add grafana as a proxy client

* AuthN: for error returned

* AuthN: add tests for grafana proxy auth

* AuthN: swap order of grafan and ldap auth

* AuthN: Parse additional proxy headers in proxy client and pass down
This commit is contained in:
Karl Persson 2023-01-17 10:07:46 +01:00 committed by GitHub
parent c1d3b59643
commit b44b6fc5c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 678 additions and 33 deletions

View File

@ -25,6 +25,7 @@ const (
ClientRender = "auth.client.render"
ClientSession = "auth.client.session"
ClientForm = "auth.client.form"
ClientProxy = "auth.client.proxy"
)
const (
@ -72,6 +73,10 @@ type PasswordClient interface {
AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error)
}
type ProxyClient interface {
AuthenticateProxy(ctx context.Context, r *Request, username string, additional map[string]string) (*Identity, error)
}
type Request struct {
// OrgID will be populated by authn.Service
OrgID int64

View File

@ -64,12 +64,18 @@ func ProvideService(
s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService)
}
var proxyClients []authn.ProxyClient
var passwordClients []authn.PasswordClient
if !s.cfg.DisableLogin {
passwordClients = append(passwordClients, clients.ProvideGrafana(userService))
}
if s.cfg.LDAPEnabled {
passwordClients = append(passwordClients, clients.ProvideLDAP(cfg))
ldap := clients.ProvideLDAP(cfg)
proxyClients = append(proxyClients, ldap)
passwordClients = append(passwordClients, ldap)
}
if !s.cfg.DisableLogin {
grafana := clients.ProvideGrafana(cfg, userService)
proxyClients = append(proxyClients, grafana)
passwordClients = append(passwordClients, grafana)
}
// if we have password clients configure check if basic auth or form auth is enabled
@ -84,6 +90,15 @@ func ProvideService(
}
}
if s.cfg.AuthProxyEnabled && len(proxyClients) > 0 {
proxy, err := clients.ProvideProxy(cfg, proxyClients...)
if err != nil {
s.log.Error("failed to configure auth proxy", "err", err)
} else {
s.clients[authn.ClientProxy] = proxy
}
}
if s.cfg.JWTAuthEnabled {
s.clients[authn.ClientJWT] = clients.ProvideJWT(jwtService, cfg)
}

View File

@ -26,3 +26,16 @@ func (m MockClient) Test(ctx context.Context, r *authn.Request) bool {
}
return false
}
var _ authn.ProxyClient = new(MockProxyClient)
type MockProxyClient struct {
AuthenticateProxyFunc func(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error)
}
func (m MockProxyClient) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) {
if m.AuthenticateProxyFunc != nil {
return m.AuthenticateProxyFunc(ctx, r, username, additional)
}
return nil, nil
}

View File

@ -4,23 +4,88 @@ import (
"context"
"crypto/subtle"
"errors"
"net/mail"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var _ authn.ProxyClient = new(Grafana)
var _ authn.PasswordClient = new(Grafana)
func ProvideGrafana(userService user.Service) *Grafana {
return &Grafana{userService}
func ProvideGrafana(cfg *setting.Cfg, userService user.Service) *Grafana {
return &Grafana{cfg, userService}
}
type Grafana struct {
cfg *setting.Cfg
userService user.Service
}
func (c Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) {
identity := &authn.Identity{
AuthModule: login.AuthProxyAuthModule,
AuthID: username,
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: c.cfg.AuthProxyAutoSignUp,
},
}
switch c.cfg.AuthProxyHeaderProperty {
case "username":
identity.Login = username
addr, err := mail.ParseAddress(username)
if err == nil {
identity.Email = addr.Address
}
case "email":
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)
}
if v, ok := additional[proxyFieldName]; ok {
identity.Name = v
}
if v, ok := additional[proxyFieldEmail]; ok {
identity.Email = v
}
if v, ok := additional[proxyFieldLogin]; ok {
identity.Login = v
}
if v, ok := additional[proxyFieldRole]; ok {
role := org.RoleType(v)
if role.IsValid() {
orgID := int64(1)
if c.cfg.AutoAssignOrg && c.cfg.AutoAssignOrgId > 0 {
orgID = int64(c.cfg.AutoAssignOrgId)
}
identity.OrgID = orgID
identity.OrgRoles = map[int64]org.RoleType{orgID: role}
}
}
if v, ok := additional[proxyFieldGroups]; ok {
identity.Groups = util.SplitString(v)
}
identity.ClientParams.LookUpParams.Email = &identity.Email
identity.ClientParams.LookUpParams.Login = &identity.Login
return identity, nil
}
func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username})
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {

View File

@ -2,16 +2,125 @@ package clients
import (
"context"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestGrafana_AuthenticateProxy(t *testing.T) {
type testCase struct {
desc string
req *authn.Request
username string
proxyProperty string
additional map[string]string
expectedErr error
expectedIdentity *authn.Identity
}
tests := []testCase{
{
desc: "expect valid identity",
username: "test",
req: &authn.Request{HTTPRequest: &http.Request{}},
proxyProperty: "username",
additional: map[string]string{
proxyFieldName: "name",
proxyFieldRole: "Viewer",
proxyFieldGroups: "grp1,grp2",
proxyFieldEmail: "email@email.com",
},
expectedIdentity: &authn.Identity{
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "name",
Email: "email@email.com",
AuthModule: "authproxy",
AuthID: "test",
Groups: []string{"grp1", "grp2"},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: true,
LookUpParams: models.UserLookupParams{
Email: strPtr("email@email.com"),
Login: strPtr("test"),
},
},
},
},
{
desc: "should set email as both email and login when configured proxy auth header property is email",
username: "test@test.com",
req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}},
additional: map[string]string{},
expectedIdentity: &authn.Identity{
Login: "test@test.com",
Email: "test@test.com",
AuthModule: "authproxy",
AuthID: "test@test.com",
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: true,
LookUpParams: models.UserLookupParams{
Email: strPtr("test@test.com"),
Login: strPtr("test@test.com"),
},
},
},
proxyProperty: "email",
},
{
desc: "should return error on invalid auth proxy header property",
req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}},
proxyProperty: "other",
expectedErr: errInvalidProxyHeader,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AuthProxyAutoSignUp = true
cfg.AuthProxyHeaderProperty = tt.proxyProperty
c := ProvideGrafana(cfg, usertest.NewUserServiceFake())
identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional)
assert.ErrorIs(t, err, tt.expectedErr)
if tt.expectedIdentity != nil {
assert.Equal(t, tt.expectedIdentity.OrgID, identity.OrgID)
assert.Equal(t, tt.expectedIdentity.Login, identity.Login)
assert.Equal(t, tt.expectedIdentity.Name, identity.Name)
assert.Equal(t, tt.expectedIdentity.Email, identity.Email)
assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID)
assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule)
assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups)
assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser)
assert.Equal(t, tt.expectedIdentity.ClientParams.AllowSignUp, identity.ClientParams.AllowSignUp)
assert.Equal(t, tt.expectedIdentity.ClientParams.SyncTeamMembers, identity.ClientParams.SyncTeamMembers)
assert.Equal(t, tt.expectedIdentity.ClientParams.EnableDisabledUsers, identity.ClientParams.EnableDisabledUsers)
assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Email, identity.ClientParams.LookUpParams.Email)
assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Login, identity.ClientParams.LookUpParams.Login)
assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.UserID, identity.ClientParams.LookUpParams.UserID)
} else {
assert.Nil(t, tt.expectedIdentity)
}
})
}
}
func TestGrafana_AuthenticatePassword(t *testing.T) {
type testCase struct {
desc string
@ -60,7 +169,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) {
userService.ExpectedError = user.ErrUserNotFound
}
c := ProvideGrafana(userService)
c := ProvideGrafana(setting.NewCfg(), userService)
identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password)
assert.ErrorIs(t, err, tt.expectedErr)
assert.EqualValues(t, tt.expectedIdentity, identity)

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
var _ authn.ProxyClient = new(LDAP)
var _ authn.PasswordClient = new(LDAP)
func ProvideLDAP(cfg *setting.Cfg) *LDAP {
@ -21,6 +22,19 @@ type LDAP struct {
service ldapService
}
func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, _ map[string]string) (*authn.Identity, error) {
info, err := c.service.User(username)
if errors.Is(err, multildap.ErrDidNotFindUser) {
return nil, errIdentityNotFound.Errorf("no user found: %w", err)
}
if err != nil {
return nil, err
}
return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil
}
func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
info, err := c.service.Login(&models.LoginUserQuery{
Username: username,
@ -43,31 +57,12 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern
return nil, err
}
return &authn.Identity{
OrgID: r.OrgID,
OrgRoles: info.OrgRoles,
Login: info.Login,
Name: info.Name,
Email: info.Email,
IsGrafanaAdmin: info.IsGrafanaAdmin,
AuthModule: info.AuthModule,
AuthID: info.AuthId,
Groups: info.Groups,
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: c.cfg.LDAPAllowSignup,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
Login: &info.Login,
Email: &info.Email,
},
},
}, nil
return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil
}
type ldapService interface {
Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error)
User(username string) (*models.ExternalUserInfo, error)
}
// FIXME: remove the implementation if we convert ldap to an actual service
@ -83,3 +78,37 @@ func (s *ldapServiceImpl) Login(query *models.LoginUserQuery) (*models.ExternalU
return multildap.New(cfg.Servers).Login(query)
}
func (s *ldapServiceImpl) User(username string) (*models.ExternalUserInfo, error) {
cfg, err := multildap.GetConfig(s.cfg)
if err != nil {
return nil, err
}
user, _, err := multildap.New(cfg.Servers).User(username)
return user, err
}
func identityFromLDAPInfo(orgID int64, info *models.ExternalUserInfo, allowSignup bool) *authn.Identity {
return &authn.Identity{
OrgID: orgID,
OrgRoles: info.OrgRoles,
Login: info.Login,
Name: info.Name,
Email: info.Email,
IsGrafanaAdmin: info.IsGrafanaAdmin,
AuthModule: info.AuthModule,
AuthID: info.AuthId,
Groups: info.Groups,
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: allowSignup,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
Login: &info.Login,
Email: &info.Email,
},
},
}
}

View File

@ -8,11 +8,74 @@ import (
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
func TestLDAP_AuthenticateProxy(t *testing.T) {
type testCase struct {
desc string
username string
expectedLDAPErr error
expectedLDAPInfo *models.ExternalUserInfo
expectedErr error
expectedIdentity *authn.Identity
}
tests := []testCase{
{
desc: "should return valid identity when found by ldap service",
username: "test",
expectedLDAPInfo: &models.ExternalUserInfo{
AuthModule: login.LDAPAuthModule,
AuthId: "123",
Email: "test@test.com",
Login: "test",
Name: "test test",
Groups: []string{"1", "2"},
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
},
expectedIdentity: &authn.Identity{
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test",
Name: "test test",
Email: "test@test.com",
AuthModule: login.LDAPAuthModule,
AuthID: "123",
Groups: []string{"1", "2"},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: false,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
Email: strPtr("test@test.com"),
Login: strPtr("test"),
},
},
},
},
{
desc: "should return error when user is not found",
username: "test",
expectedLDAPErr: multildap.ErrDidNotFindUser,
expectedErr: errIdentityNotFound,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedLDAPInfo, ExpectedErr: tt.expectedLDAPErr}}
identity, err := c.AuthenticateProxy(context.Background(), &authn.Request{OrgID: 1}, tt.username, nil)
assert.ErrorIs(t, err, tt.expectedErr)
assert.EqualValues(t, tt.expectedIdentity, identity)
})
}
}
func TestLDAP_AuthenticatePassword(t *testing.T) {
type testCase struct {
desc string
@ -20,7 +83,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
password string
expectedErr error
expectedLDAPErr error
expectedInfo *models.ExternalUserInfo
expectedLDAPInfo *models.ExternalUserInfo
expectedIdentity *authn.Identity
}
@ -29,7 +92,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
desc: "should successfully authenticate with correct username and password",
username: "test",
password: "test123",
expectedInfo: &models.ExternalUserInfo{
expectedLDAPInfo: &models.ExternalUserInfo{
AuthModule: login.LDAPAuthModule,
AuthId: "123",
Email: "test@test.com",
@ -77,7 +140,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedInfo, ExpectedErr: tt.expectedLDAPErr}}
c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedLDAPInfo, ExpectedErr: tt.expectedLDAPErr}}
identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password)
assert.ErrorIs(t, err, tt.expectedErr)
@ -100,3 +163,7 @@ type fakeLDAPService struct {
func (f fakeLDAPService) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) {
return f.ExpectedInfo, f.ExpectedErr
}
func (f fakeLDAPService) User(username string) (*models.ExternalUserInfo, error) {
return f.ExpectedInfo, f.ExpectedErr
}

View File

@ -0,0 +1,147 @@
package clients
import (
"context"
"fmt"
"net"
"path"
"strings"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
)
const (
proxyFieldName = "Name"
proxyFieldEmail = "Email"
proxyFieldLogin = "Login"
proxyFieldRole = "Role"
proxyFieldGroups = "Groups"
)
var proxyFields = [...]string{proxyFieldName, proxyFieldEmail, proxyFieldLogin, proxyFieldRole, proxyFieldGroups}
var (
errNotAcceptedIP = errutil.NewBase(errutil.StatusUnauthorized, "auth-proxy.invalid-ip")
errEmptyProxyHeader = errutil.NewBase(errutil.StatusUnauthorized, "auth-proxy.empty-header")
errInvalidProxyHeader = errutil.NewBase(errutil.StatusInternal, "auth-proxy.invalid-proxy-header")
)
var _ authn.Client = new(Proxy)
func ProvideProxy(cfg *setting.Cfg, clients ...authn.ProxyClient) (*Proxy, error) {
list, err := parseAcceptList(cfg.AuthProxyWhitelist)
if err != nil {
return nil, err
}
return &Proxy{cfg, clients, list}, nil
}
type Proxy struct {
cfg *setting.Cfg
clients []authn.ProxyClient
acceptedIPs []*net.IPNet
}
func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
if !c.isAllowedIP(r) {
return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list")
}
username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)
if len(username) == 0 {
return nil, errEmptyProxyHeader.Errorf("no username provided in auth proxy header")
}
additional := getAdditionalProxyHeaders(r, c.cfg)
// FIXME: add cache to prevent sync on every request
var clientErr error
for _, proxyClient := range c.clients {
var identity *authn.Identity
identity, clientErr = proxyClient.AuthenticateProxy(ctx, r, username, additional)
if identity != nil {
return identity, nil
}
}
return nil, clientErr
}
func (c *Proxy) Test(ctx context.Context, r *authn.Request) bool {
return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0
}
func (c *Proxy) isAllowedIP(r *authn.Request) bool {
if len(c.acceptedIPs) == 0 {
return true
}
host, _, err := net.SplitHostPort(r.HTTPRequest.RemoteAddr)
if err != nil {
return false
}
ip := net.ParseIP(host)
for _, v := range c.acceptedIPs {
if v.Contains(ip) {
return true
}
}
return false
}
func parseAcceptList(s string) ([]*net.IPNet, error) {
if len(strings.TrimSpace(s)) == 0 {
return nil, nil
}
addresses := strings.Split(s, ",")
list := make([]*net.IPNet, 0, len(addresses))
for _, addr := range addresses {
result, err := coerceProxyAddress(addr)
if err != nil {
return nil, err
}
list = append(list, result)
}
return list, nil
}
// coerceProxyAddress gets network of the presented CIDR notation
func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) {
proxyAddr = strings.TrimSpace(proxyAddr)
if !strings.Contains(proxyAddr, "/") {
proxyAddr = path.Join(proxyAddr, "32")
}
_, network, err := net.ParseCIDR(proxyAddr)
if err != nil {
return nil, fmt.Errorf("could not parse the network: %w", err)
}
return network, nil
}
func getProxyHeader(r *authn.Request, headerName string, encoded bool) string {
if r.HTTPRequest == nil {
return ""
}
v := r.HTTPRequest.Header.Get(headerName)
if encoded {
v = util.DecodeQuotedPrintable(v)
}
return v
}
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 != "" {
additional[k] = v
}
}
return additional
}

View File

@ -0,0 +1,172 @@
package clients
import (
"context"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/authn"
"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.AuthProxyHeaderName = "X-Username"
cfg.AuthProxyHeaders = tt.proxyHeaders
cfg.AuthProxyWhitelist = 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, 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.AuthProxyHeaderName = "Proxy-Header"
c, _ := ProvideProxy(cfg, nil)
assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req))
})
}
}

View File

@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/rendering"
@ -106,7 +107,7 @@ func getContextHandler(t *testing.T) *ContextHandler {
return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc,
renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator,
&userService, orgService, nil, nil, &authntest.FakeService{})
&userService, orgService, nil, featuremgmt.WithFeatures(), &authntest.FakeService{})
}
type fakeAuthenticator struct{}

View File

@ -714,6 +714,28 @@ func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCo
}
func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, orgID int64) bool {
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientProxy, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
if !ok {
return false
}
if err != nil {
writeErr(reqContext, err)
}
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName)
for _, header := range h.Cfg.AuthProxyHeaders {
if header != "" {
ctx = WithAuthHTTPHeader(ctx, header)
}
}
*reqContext.Req = *reqContext.Req.WithContext(ctx)
reqContext.IsSignedIn = true
reqContext.SignedInUser = identity.SignedInUser()
return true
}
username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName)
logger := log.New("auth.proxy")