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 }