Files
grafana/pkg/middleware/auth_proxy/auth_proxy.go
T
Arve Knudsen a04ef6cefc 6.7.3 cherry-picks (#23808)
* AuthProxy: Fixes bug where long username could not be cached (#22926)

(cherry picked from commit 6c9d833602)

* Server: Exit with 0 if no error (#23312)

Make grafana-server exit with 0 if no error occurred.

(cherry picked from commit 5645d74cbc)

* Dashboard: Save json should preserve folderId (#23314)

(cherry picked from commit 7e3b43eabb)

* TimeSrv: Try to parse 8 and 15 digit numbers as timestamps if parsing as date fails (#21694)

* Try to parse 8 and 15 digit numbers as timestamps if parsing as date fails

Fixes #19738

* Add tests

(cherry picked from commit c89ad9b038)

* BackendSrv: include credentials when withCredentials option is set (#23380)

The fetch() API won't send cookies or other type of credentials unless
you set the credentials init option. Some datasources like Prometheus
and Elasticsearch have `withCredentials` option in Browser access mode,
but this option is not currently getting passed in the fetch() API.

Fixes #23338.

(cherry picked from commit afd8ffde69)

* Dashlist: Fixed dashlist broken in edit mode (#23426)

(cherry picked from commit 363bf7506d)

* Admin: Fix Synced via LDAP message for non-LDAP external users (#23477)

* UserAdmin: remove Synced via LDAP message for non-LDAP users

* UserAdmin: show "Synced via <provider>" message for external users

(cherry picked from commit 4d81cec34f)

* Graphite: Fixed cannot read finally of undefiend (#23512)

(cherry picked from commit 61460ea3a2)

* Hangouts: fixes notifications for alerts with empty message (#23559)

* Hangouts: fixes notifications for alerts with empty message

* Update pkg/services/alerting/notifiers/googlechat.go

Co-Authored-By: Marcus Efraimsson <marcus.efraimsson@gmail.com>

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
(cherry picked from commit 2661054fe8)

* Variables: fixes error when setting adhoc variables values (#23580)

(cherry picked from commit 0091885b13)

* Release 6.7.3

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* ci-metrics-publisher.sh: Fix linting issue

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* TablePanel: Fix XSS issue in header column rename (backport) (#23814)

* escaping html when rendering table header alias.

* fixed tooltip.

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* Security: Fix annotation popup XSS vulnerability (#23813)

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
(cherry picked from commit 3955e8cbad)

Co-authored-by: Jon McKenzie <jcmcken@gmail.com>
Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
Co-authored-by: Jesse Tan <jessetan@users.noreply.github.com>
Co-authored-by: Tuan Anh Hoang-Vu <hvtuananh@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
2020-04-23 12:12:53 +02:00

352 lines
8.2 KiB
Go

package authproxy
import (
"encoding/hex"
"fmt"
"hash/fnv"
"net"
"net/mail"
"reflect"
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
// CachePrefix is a prefix for the cache key
CachePrefix = "auth-proxy-sync-ttl:%s"
)
// getLDAPConfig gets LDAP config
var getLDAPConfig = ldap.GetConfig
// isLDAPEnabled checks if LDAP is enabled
var isLDAPEnabled = ldap.IsEnabled
// newLDAP creates multiple LDAP instance
var newLDAP = multildap.New
// supportedHeaders states the supported headers configuration fields
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"}
// AuthProxy struct
type AuthProxy struct {
store *remotecache.RemoteCache
ctx *models.ReqContext
orgID int64
header string
enabled bool
LDAPAllowSignup bool
AuthProxyAutoSignUp bool
whitelistIP string
headerType string
headers map[string]string
cacheTTL int
}
// Error auth proxy specific error
type Error struct {
Message string
DetailsError error
}
// newError creates the Error
func newError(message string, err error) *Error {
return &Error{
Message: message,
DetailsError: err,
}
}
// Error returns a Error error string
func (err *Error) Error() string {
return err.Message
}
// Options for the AuthProxy
type Options struct {
Store *remotecache.RemoteCache
Ctx *models.ReqContext
OrgID int64
}
// New instance of the AuthProxy
func New(options *Options) *AuthProxy {
header := options.Ctx.Req.Header.Get(setting.AuthProxyHeaderName)
return &AuthProxy{
store: options.Store,
ctx: options.Ctx,
orgID: options.OrgID,
header: header,
enabled: setting.AuthProxyEnabled,
headerType: setting.AuthProxyHeaderProperty,
headers: setting.AuthProxyHeaders,
whitelistIP: setting.AuthProxyWhitelist,
cacheTTL: setting.AuthProxySyncTtl,
LDAPAllowSignup: setting.LDAPAllowSignup,
AuthProxyAutoSignUp: setting.AuthProxyAutoSignUp,
}
}
// IsEnabled checks if the proxy auth is enabled
func (auth *AuthProxy) IsEnabled() bool {
// Bail if the setting is not enabled
return auth.enabled
}
// HasHeader checks if the we have specified header
func (auth *AuthProxy) HasHeader() bool {
return len(auth.header) != 0
}
// IsAllowedIP compares presented IP with the whitelist one
func (auth *AuthProxy) IsAllowedIP() (bool, *Error) {
ip := auth.ctx.Req.RemoteAddr
if len(strings.TrimSpace(auth.whitelistIP)) == 0 {
return true, nil
}
proxies := strings.Split(auth.whitelistIP, ",")
var proxyObjs []*net.IPNet
for _, proxy := range proxies {
result, err := coerceProxyAddress(proxy)
if err != nil {
return false, newError("Could not get the network", err)
}
proxyObjs = append(proxyObjs, result)
}
sourceIP, _, _ := net.SplitHostPort(ip)
sourceObj := net.ParseIP(sourceIP)
for _, proxyObj := range proxyObjs {
if proxyObj.Contains(sourceObj) {
return true, nil
}
}
err := fmt.Errorf(
"Request for user (%s) from %s is not from the authentication proxy", auth.header,
sourceIP,
)
return false, newError("Proxy authentication required", err)
}
func HashCacheKey(key string) string {
hasher := fnv.New128a()
// according to the documentation, Hash.Write cannot error, but linter is complaining
hasher.Write([]byte(key)) // nolint: errcheck
return hex.EncodeToString(hasher.Sum(nil))
}
// getKey forms a key for the cache based on the headers received as part of the authentication flow.
// Our configuration supports multiple headers. The main header contains the email or username.
// And the additional ones that allow us to specify extra attributes: Name, Email or Groups.
func (auth *AuthProxy) getKey() string {
key := strings.TrimSpace(auth.header) // start the key with the main header
auth.headersIterator(func(_, header string) {
key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
})
hashedKey := HashCacheKey(key)
return fmt.Sprintf(CachePrefix, hashedKey)
}
// Login logs in user id with whatever means possible
func (auth *AuthProxy) Login() (int64, *Error) {
id, _ := auth.GetUserViaCache()
if id != 0 {
// Error here means absent cache - we don't need to handle that
return id, nil
}
if isLDAPEnabled() {
id, err := auth.LoginViaLDAP()
if err == ldap.ErrInvalidCredentials {
return 0, newError(
"Proxy authentication required",
ldap.ErrInvalidCredentials,
)
}
if err != nil {
return 0, newError("Failed to get the user", err)
}
return id, nil
}
id, err := auth.LoginViaHeader()
if err != nil {
return 0, newError(
"Failed to log in as user, specified in auth proxy header",
err,
)
}
return id, nil
}
// GetUserViaCache gets user id from cache
func (auth *AuthProxy) GetUserViaCache() (int64, error) {
var (
cacheKey = auth.getKey()
userID, err = auth.store.Get(cacheKey)
)
if err != nil {
return 0, err
}
return userID.(int64), nil
}
// LoginViaLDAP logs in user via LDAP request
func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
config, err := getLDAPConfig()
if err != nil {
return 0, newError("Failed to get LDAP config", nil)
}
extUser, _, err := newLDAP(config.Servers).User(auth.header)
if err != nil {
return 0, newError(err.Error(), nil)
}
// Have to sync grafana and LDAP user during log in
upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx,
SignupAllowed: auth.LDAPAllowSignup,
ExternalUser: extUser,
}
err = bus.Dispatch(upsert)
if err != nil {
return 0, newError(err.Error(), nil)
}
return upsert.Result.Id, nil
}
// LoginViaHeader logs in user from the header only
func (auth *AuthProxy) LoginViaHeader() (int64, error) {
extUser := &models.ExternalUserInfo{
AuthModule: "authproxy",
AuthId: auth.header,
}
switch auth.headerType {
case "username":
extUser.Login = auth.header
emailAddr, emailErr := mail.ParseAddress(auth.header) // only set Email if it can be parsed as an email address
if emailErr == nil {
extUser.Email = emailAddr.Address
}
case "email":
extUser.Email = auth.header
extUser.Login = auth.header
default:
return 0, newError("Auth proxy header property invalid", nil)
}
auth.headersIterator(func(field string, header string) {
if field == "Groups" {
extUser.Groups = util.SplitString(header)
} else {
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
}
})
upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx,
SignupAllowed: setting.AuthProxyAutoSignUp,
ExternalUser: extUser,
}
err := bus.Dispatch(upsert)
if err != nil {
return 0, err
}
return upsert.Result.Id, nil
}
// headersIterator iterates over all non-empty supported additional headers
func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
for _, field := range supportedHeaderFields {
h := auth.headers[field]
if h == "" {
continue
}
if value := auth.ctx.Req.Header.Get(h); value != "" {
fn(field, strings.TrimSpace(value))
}
}
}
// GetSignedUser get full signed user info
func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error) {
query := &models.GetSignedInUserQuery{
OrgId: auth.orgID,
UserId: userID,
}
if err := bus.Dispatch(query); err != nil {
return nil, newError(err.Error(), nil)
}
return query.Result, nil
}
// Remember user in cache
func (auth *AuthProxy) Remember(id int64) *Error {
key := auth.getKey()
// Check if user already in cache
userID, _ := auth.store.Get(key)
if userID != nil {
return nil
}
expiration := time.Duration(auth.cacheTTL) * time.Minute
err := auth.store.Set(key, id, expiration)
if err != nil {
return newError(err.Error(), nil)
}
return 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 = strings.Join([]string{proxyAddr, "32"}, "/")
}
_, network, err := net.ParseCIDR(proxyAddr)
return network, err
}