| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- 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
- }
|