grafana/pkg/middleware/auth_proxy/auth_proxy.go
gotjosh 0cbbb43222
LDAP: Add API endpoint to debug user mapping from LDAP (#18833)
* Move the ReloadLDAPCfg function to the debug file

Appears to be a better suite place for this.

* LDAP: Return the server information when we find a specific user

We allow you to specify multiple LDAP servers as part of LDAP authentication integration. As part of searching for specific users, we need to understand from which server they come from. Returning the server configuration as part of the search will help us do two things:

- Understand in which server we found the user
- Have access the groups specified as part of the server configuration

* LDAP: Adds the /api/admin/ldap/:username endpoint

This endpoint returns a user found within the configured LDAP server(s). Moreso, it provides the mapping information for the user to help administrators understand how the users would be created within Grafana based on the current configuration.

No changes are executed or saved to the database, this is all an in-memory representation of how the final result would look like.
2019-09-03 18:34:44 +01:00

344 lines
8.0 KiB
Go

package authproxy
import (
"encoding/base32"
"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/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.AuthProxyLDAPSyncTtl,
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)
}
// 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 := base32.StdEncoding.EncodeToString([]byte(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
}