Files
Samuel 846b9327a5 LDAP: Added reload endpoint for LDAP config (#15470)
* 4843 - Added reload endpoint for LDAP config

closes #4843

* Refactor to make the reload work after master drifted
2019-05-03 14:53:07 +02:00

333 lines
6.9 KiB
Go

package authproxy
import (
"fmt"
"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/setting"
)
const (
// CachePrefix is a prefix for the cache key
CachePrefix = "auth-proxy-sync-ttl:%s"
)
var (
getLDAPConfig = ldap.GetConfig
isLDAPEnabled = ldap.IsEnabled
)
// AuthProxy struct
type AuthProxy struct {
store *remotecache.RemoteCache
ctx *models.ReqContext
orgID int64
header string
LDAP func(server *ldap.ServerConfig) ldap.IAuth
enabled 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 fmt.Sprintf("%s", 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,
LDAP: ldap.New,
enabled: setting.AuthProxyEnabled,
headerType: setting.AuthProxyHeaderProperty,
headers: setting.AuthProxyHeaders,
whitelistIP: setting.AuthProxyWhitelist,
cacheTTL: setting.AuthProxyLdapSyncTtl,
}
}
// IsEnabled checks if the proxy auth is enabled
func (auth *AuthProxy) IsEnabled() bool {
// Bail if the setting is not enabled
if auth.enabled == false {
return false
}
return true
}
// HasHeader checks if the we have specified header
func (auth *AuthProxy) HasHeader() bool {
if len(auth.header) == 0 {
return false
}
return true
}
// 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)
}
// InCache checks if we have user in cache
func (auth *AuthProxy) InCache() bool {
userID, _ := auth.GetUserIDViaCache()
if userID == 0 {
return false
}
return true
}
// getKey forms a key for the cache
func (auth *AuthProxy) getKey() string {
return fmt.Sprintf(CachePrefix, auth.header)
}
// GetUserID gets user id with whatever means possible
func (auth *AuthProxy) GetUserID() (int64, *Error) {
if auth.InCache() {
// Error here means absent cache - we don't need to handle that
id, _ := auth.GetUserIDViaCache()
return id, nil
}
if isLDAPEnabled() {
id, err := auth.GetUserIDViaLDAP()
if err == ldap.ErrInvalidCredentials {
return 0, newError(
"Proxy authentication required",
ldap.ErrInvalidCredentials,
)
}
if err != nil {
return 0, newError("Failed to sync user", err)
}
return id, nil
}
id, err := auth.GetUserIDViaHeader()
if err != nil {
return 0, newError(
"Failed to login as user specified in auth proxy header",
err,
)
}
return id, nil
}
// GetUserIDViaCache gets the user from cache
func (auth *AuthProxy) GetUserIDViaCache() (int64, error) {
var (
cacheKey = auth.getKey()
userID, err = auth.store.Get(cacheKey)
)
if err != nil {
return 0, err
}
return userID.(int64), nil
}
// GetUserIDViaLDAP gets user via LDAP request
func (auth *AuthProxy) GetUserIDViaLDAP() (int64, *Error) {
query := &models.LoginUserQuery{
ReqContext: auth.ctx,
Username: auth.header,
}
config, err := getLDAPConfig()
if err != nil {
return 0, newError("Failed to get LDAP config", nil)
}
if len(config.Servers) == 0 {
return 0, newError("No LDAP servers available", nil)
}
for _, server := range config.Servers {
author := auth.LDAP(server)
if err := author.SyncUser(query); err != nil {
return 0, newError(err.Error(), nil)
}
}
return query.User.Id, nil
}
// GetUserIDViaHeader gets user from the header only
func (auth *AuthProxy) GetUserIDViaHeader() (int64, error) {
extUser := &models.ExternalUserInfo{
AuthModule: "authproxy",
AuthId: auth.header,
}
if auth.headerType == "username" {
extUser.Login = auth.header
// only set Email if it can be parsed as an email address
emailAddr, emailErr := mail.ParseAddress(auth.header)
if emailErr == nil {
extUser.Email = emailAddr.Address
}
} else if auth.headerType == "email" {
extUser.Email = auth.header
extUser.Login = auth.header
} else {
return 0, newError("Auth proxy header property invalid", nil)
}
for _, field := range []string{"Name", "Email", "Login"} {
if auth.headers[field] == "" {
continue
}
if val := auth.ctx.Req.Header.Get(auth.headers[field]); val != "" {
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
}
}
// add/update user in grafana
cmd := &models.UpsertUserCommand{
ReqContext: auth.ctx,
ExternalUser: extUser,
SignupAllowed: setting.AuthProxyAutoSignUp,
}
err := bus.Dispatch(cmd)
if err != nil {
return 0, err
}
return cmd.Result.Id, nil
}
// 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() *Error {
// Make sure we do not rewrite the expiration time
if auth.InCache() {
return nil
}
var (
key = auth.getKey()
value, _ = auth.GetUserIDViaCache()
expiration = time.Duration(-auth.cacheTTL) * time.Minute
err = auth.store.Set(key, value, 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
}