mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 01:53:33 -06:00
* UserService: update callers to use the UserService instead of calling sqlstore directly There is one major change hiding in this PR. UserService.Delete originally called a number of services to delete user-related records. I moved everything except the actual call to the user table, and moved those into the API. This was done to avoid dependencies cycles; many of our services depend on the user service, so the user service itself should have as few dependencies as possible.
393 lines
10 KiB
Go
393 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
|
|
userService user.Service
|
|
|
|
logger log.Logger
|
|
}
|
|
|
|
func ProvideAuthProxy(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, loginService login.Service, userService user.Service, sqlStore sqlstore.Store) *AuthProxy {
|
|
return &AuthProxy{
|
|
cfg: cfg,
|
|
remoteCache: remoteCache,
|
|
loginService: loginService,
|
|
sqlStore: sqlStore,
|
|
userService: userService,
|
|
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) {
|
|
return auth.userService.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
|
|
OrgID: orgID,
|
|
UserID: userID,
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|