mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 01:53:33 -06:00
* Move SignedInUser to user service and RoleType and Roles to org * Use go naming convention for roles * Fix some imports and leftovers * Fix ldap debug test * Fix lint * Fix lint 2 * Fix lint 3 * Fix type and not needed conversion * Clean up messages in api tests * Clean up api tests 2
397 lines
10 KiB
Go
397 lines
10 KiB
Go
package authproxy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"net"
|
|
"net/mail"
|
|
"path"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"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/login"
|
|
"github.com/grafana/grafana/pkg/services/multildap"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"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 = func(cfg *setting.Cfg) bool {
|
|
if cfg != nil {
|
|
return cfg.LDAPEnabled
|
|
}
|
|
|
|
return setting.LDAPEnabled
|
|
}
|
|
|
|
// newLDAP creates multiple LDAP instance
|
|
var newLDAP = multildap.New
|
|
|
|
// supportedHeaders states the supported headers configuration fields
|
|
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups", "Role"}
|
|
|
|
// AuthProxy struct
|
|
type AuthProxy struct {
|
|
cfg *setting.Cfg
|
|
remoteCache *remotecache.RemoteCache
|
|
loginService login.Service
|
|
sqlStore sqlstore.Store
|
|
|
|
logger log.Logger
|
|
}
|
|
|
|
func ProvideAuthProxy(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, loginService login.Service, sqlStore sqlstore.Store) *AuthProxy {
|
|
return &AuthProxy{
|
|
cfg: cfg,
|
|
remoteCache: remoteCache,
|
|
loginService: loginService,
|
|
sqlStore: sqlStore,
|
|
logger: log.New("auth.proxy"),
|
|
}
|
|
}
|
|
|
|
// Error auth proxy specific error
|
|
type Error struct {
|
|
Message string
|
|
DetailsError error
|
|
}
|
|
|
|
// newError returns an Error.
|
|
func newError(message string, err error) Error {
|
|
return Error{
|
|
Message: message,
|
|
DetailsError: err,
|
|
}
|
|
}
|
|
|
|
// Error returns the error message.
|
|
func (err Error) Error() string {
|
|
return err.Message
|
|
}
|
|
|
|
// IsEnabled checks if the auth proxy is enabled.
|
|
func (auth *AuthProxy) IsEnabled() bool {
|
|
// Bail if the setting is not enabled
|
|
return auth.cfg.AuthProxyEnabled
|
|
}
|
|
|
|
// HasHeader checks if we have specified header
|
|
func (auth *AuthProxy) HasHeader(reqCtx *models.ReqContext) bool {
|
|
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
|
|
return len(header) != 0
|
|
}
|
|
|
|
// IsAllowedIP returns whether provided IP is allowed.
|
|
func (auth *AuthProxy) IsAllowedIP(ip string) error {
|
|
if len(strings.TrimSpace(auth.cfg.AuthProxyWhitelist)) == 0 {
|
|
return nil
|
|
}
|
|
|
|
proxies := strings.Split(auth.cfg.AuthProxyWhitelist, ",")
|
|
var proxyObjs []*net.IPNet
|
|
for _, proxy := range proxies {
|
|
result, err := coerceProxyAddress(proxy)
|
|
if err != nil {
|
|
return newError("could not get the network", err)
|
|
}
|
|
|
|
proxyObjs = append(proxyObjs, result)
|
|
}
|
|
|
|
sourceIP, _, err := net.SplitHostPort(ip)
|
|
if err != nil {
|
|
return newError("could not parse address", err)
|
|
}
|
|
sourceObj := net.ParseIP(sourceIP)
|
|
|
|
for _, proxyObj := range proxyObjs {
|
|
if proxyObj.Contains(sourceObj) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return newError("proxy authentication required", fmt.Errorf(
|
|
"request for user from %s is not from the authentication proxy",
|
|
sourceIP,
|
|
))
|
|
}
|
|
|
|
func HashCacheKey(key string) (string, error) {
|
|
hasher := fnv.New128a()
|
|
if _, err := hasher.Write([]byte(key)); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(hasher.Sum(nil)), 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, Role, or Groups.
|
|
func (auth *AuthProxy) getKey(reqCtx *models.ReqContext) (string, error) {
|
|
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
|
|
key := strings.TrimSpace(header) // start the key with the main header
|
|
|
|
auth.headersIterator(reqCtx, func(_, header string) {
|
|
key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
|
|
})
|
|
|
|
hashedKey, err := HashCacheKey(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf(CachePrefix, hashedKey), nil
|
|
}
|
|
|
|
// Login logs in user ID by whatever means possible.
|
|
func (auth *AuthProxy) Login(reqCtx *models.ReqContext, ignoreCache bool) (int64, error) {
|
|
if !ignoreCache {
|
|
// Error here means absent cache - we don't need to handle that
|
|
id, err := auth.getUserViaCache(reqCtx)
|
|
if err == nil && id != 0 {
|
|
return id, nil
|
|
}
|
|
}
|
|
|
|
if isLDAPEnabled(auth.cfg) {
|
|
id, err := auth.LoginViaLDAP(reqCtx)
|
|
if err != nil {
|
|
if errors.Is(err, ldap.ErrInvalidCredentials) {
|
|
return 0, newError("proxy authentication required", ldap.ErrInvalidCredentials)
|
|
}
|
|
return 0, newError("failed to get the user", err)
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
id, err := auth.loginViaHeader(reqCtx)
|
|
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(reqCtx *models.ReqContext) (int64, error) {
|
|
cacheKey, err := auth.getKey(reqCtx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
auth.logger.Debug("Getting user ID via auth cache", "cacheKey", cacheKey)
|
|
userID, err := auth.remoteCache.Get(reqCtx.Req.Context(), cacheKey)
|
|
if err != nil {
|
|
auth.logger.Debug("Failed getting user ID via auth cache", "error", err)
|
|
return 0, err
|
|
}
|
|
|
|
auth.logger.Debug("Successfully got user ID via auth cache", "id", userID)
|
|
return userID.(int64), nil
|
|
}
|
|
|
|
// RemoveUserFromCache removes user from cache.
|
|
func (auth *AuthProxy) RemoveUserFromCache(reqCtx *models.ReqContext) error {
|
|
cacheKey, err := auth.getKey(reqCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
auth.logger.Debug("Removing user from auth cache", "cacheKey", cacheKey)
|
|
if err := auth.remoteCache.Delete(reqCtx.Req.Context(), cacheKey); err != nil {
|
|
return err
|
|
}
|
|
|
|
auth.logger.Debug("Successfully removed user from auth cache", "cacheKey", cacheKey)
|
|
return nil
|
|
}
|
|
|
|
// LoginViaLDAP logs in user via LDAP request
|
|
func (auth *AuthProxy) LoginViaLDAP(reqCtx *models.ReqContext) (int64, error) {
|
|
config, err := getLDAPConfig(auth.cfg)
|
|
if err != nil {
|
|
return 0, newError("failed to get LDAP config", err)
|
|
}
|
|
|
|
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
|
|
mldap := newLDAP(config.Servers)
|
|
extUser, _, err := mldap.User(header)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Have to sync grafana and LDAP user during log in
|
|
upsert := &models.UpsertUserCommand{
|
|
ReqContext: reqCtx,
|
|
SignupAllowed: auth.cfg.LDAPAllowSignup,
|
|
ExternalUser: extUser,
|
|
UserLookupParams: models.UserLookupParams{
|
|
Login: &extUser.Login,
|
|
Email: &extUser.Email,
|
|
UserID: nil,
|
|
},
|
|
}
|
|
if err := auth.loginService.UpsertUser(reqCtx.Req.Context(), upsert); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return upsert.Result.ID, nil
|
|
}
|
|
|
|
// loginViaHeader logs in user from the header only
|
|
func (auth *AuthProxy) loginViaHeader(reqCtx *models.ReqContext) (int64, error) {
|
|
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
|
|
extUser := &models.ExternalUserInfo{
|
|
AuthModule: login.AuthProxyAuthModule,
|
|
AuthId: header,
|
|
}
|
|
|
|
switch auth.cfg.AuthProxyHeaderProperty {
|
|
case "username":
|
|
extUser.Login = header
|
|
|
|
emailAddr, emailErr := mail.ParseAddress(header) // only set Email if it can be parsed as an email address
|
|
if emailErr == nil {
|
|
extUser.Email = emailAddr.Address
|
|
}
|
|
case "email":
|
|
extUser.Email = header
|
|
extUser.Login = header
|
|
default:
|
|
return 0, fmt.Errorf("auth proxy header property invalid")
|
|
}
|
|
|
|
auth.headersIterator(reqCtx, func(field string, header string) {
|
|
switch field {
|
|
case "Groups":
|
|
extUser.Groups = util.SplitString(header)
|
|
case "Role":
|
|
// If Role header is specified, we update the user role of the default org
|
|
if header != "" {
|
|
rt := org.RoleType(header)
|
|
if rt.IsValid() {
|
|
extUser.OrgRoles = map[int64]org.RoleType{}
|
|
orgID := int64(1)
|
|
if setting.AutoAssignOrg && setting.AutoAssignOrgId > 0 {
|
|
orgID = int64(setting.AutoAssignOrgId)
|
|
}
|
|
extUser.OrgRoles[orgID] = rt
|
|
}
|
|
}
|
|
default:
|
|
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
|
|
}
|
|
})
|
|
|
|
upsert := &models.UpsertUserCommand{
|
|
ReqContext: reqCtx,
|
|
SignupAllowed: auth.cfg.AuthProxyAutoSignUp,
|
|
ExternalUser: extUser,
|
|
UserLookupParams: models.UserLookupParams{
|
|
UserID: nil,
|
|
Login: &extUser.Login,
|
|
Email: &extUser.Email,
|
|
},
|
|
}
|
|
|
|
err := auth.loginService.UpsertUser(reqCtx.Req.Context(), upsert)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return upsert.Result.ID, nil
|
|
}
|
|
|
|
// getDecodedHeader gets decoded value of a header with given headerName
|
|
func (auth *AuthProxy) getDecodedHeader(reqCtx *models.ReqContext, headerName string) string {
|
|
headerValue := reqCtx.Req.Header.Get(headerName)
|
|
|
|
if auth.cfg.AuthProxyHeadersEncoded {
|
|
headerValue = util.DecodeQuotedPrintable(headerValue)
|
|
}
|
|
|
|
return headerValue
|
|
}
|
|
|
|
// headersIterator iterates over all non-empty supported additional headers
|
|
func (auth *AuthProxy) headersIterator(reqCtx *models.ReqContext, fn func(field string, header string)) {
|
|
for _, field := range supportedHeaderFields {
|
|
h := auth.cfg.AuthProxyHeaders[field]
|
|
if h == "" {
|
|
continue
|
|
}
|
|
|
|
if value := auth.getDecodedHeader(reqCtx, h); value != "" {
|
|
fn(field, strings.TrimSpace(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetSignedInUser gets full signed in user info.
|
|
func (auth *AuthProxy) GetSignedInUser(userID int64, orgID int64) (*user.SignedInUser, error) {
|
|
query := &models.GetSignedInUserQuery{
|
|
OrgId: orgID,
|
|
UserId: userID,
|
|
}
|
|
|
|
if err := auth.sqlStore.GetSignedInUser(context.Background(), query); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return query.Result, nil
|
|
}
|
|
|
|
// Remember user in cache
|
|
func (auth *AuthProxy) Remember(reqCtx *models.ReqContext, id int64) error {
|
|
key, err := auth.getKey(reqCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if user already in cache
|
|
userID, err := auth.remoteCache.Get(reqCtx.Req.Context(), key)
|
|
if err == nil && userID != nil {
|
|
return nil
|
|
}
|
|
|
|
expiration := time.Duration(auth.cfg.AuthProxySyncTTL) * time.Minute
|
|
|
|
if err := auth.remoteCache.Set(reqCtx.Req.Context(), key, id, expiration); err != nil {
|
|
return err
|
|
}
|
|
|
|
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 = 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
|
|
}
|