auth_proxy.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. package authproxy
  2. import (
  3. "encoding/base32"
  4. "fmt"
  5. "net"
  6. "net/mail"
  7. "reflect"
  8. "strings"
  9. "time"
  10. "github.com/grafana/grafana/pkg/bus"
  11. "github.com/grafana/grafana/pkg/infra/remotecache"
  12. "github.com/grafana/grafana/pkg/models"
  13. "github.com/grafana/grafana/pkg/services/ldap"
  14. "github.com/grafana/grafana/pkg/services/multildap"
  15. "github.com/grafana/grafana/pkg/setting"
  16. "github.com/grafana/grafana/pkg/util"
  17. )
  18. const (
  19. // CachePrefix is a prefix for the cache key
  20. CachePrefix = "auth-proxy-sync-ttl:%s"
  21. )
  22. // getLDAPConfig gets LDAP config
  23. var getLDAPConfig = ldap.GetConfig
  24. // isLDAPEnabled checks if LDAP is enabled
  25. var isLDAPEnabled = ldap.IsEnabled
  26. // newLDAP creates multiple LDAP instance
  27. var newLDAP = multildap.New
  28. // supportedHeaders states the supported headers configuration fields
  29. var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"}
  30. // AuthProxy struct
  31. type AuthProxy struct {
  32. store *remotecache.RemoteCache
  33. ctx *models.ReqContext
  34. orgID int64
  35. header string
  36. enabled bool
  37. LDAPAllowSignup bool
  38. AuthProxyAutoSignUp bool
  39. whitelistIP string
  40. headerType string
  41. headers map[string]string
  42. cacheTTL int
  43. }
  44. // Error auth proxy specific error
  45. type Error struct {
  46. Message string
  47. DetailsError error
  48. }
  49. // newError creates the Error
  50. func newError(message string, err error) *Error {
  51. return &Error{
  52. Message: message,
  53. DetailsError: err,
  54. }
  55. }
  56. // Error returns a Error error string
  57. func (err *Error) Error() string {
  58. return err.Message
  59. }
  60. // Options for the AuthProxy
  61. type Options struct {
  62. Store *remotecache.RemoteCache
  63. Ctx *models.ReqContext
  64. OrgID int64
  65. }
  66. // New instance of the AuthProxy
  67. func New(options *Options) *AuthProxy {
  68. header := options.Ctx.Req.Header.Get(setting.AuthProxyHeaderName)
  69. return &AuthProxy{
  70. store: options.Store,
  71. ctx: options.Ctx,
  72. orgID: options.OrgID,
  73. header: header,
  74. enabled: setting.AuthProxyEnabled,
  75. headerType: setting.AuthProxyHeaderProperty,
  76. headers: setting.AuthProxyHeaders,
  77. whitelistIP: setting.AuthProxyWhitelist,
  78. cacheTTL: setting.AuthProxyLDAPSyncTtl,
  79. LDAPAllowSignup: setting.LDAPAllowSignup,
  80. AuthProxyAutoSignUp: setting.AuthProxyAutoSignUp,
  81. }
  82. }
  83. // IsEnabled checks if the proxy auth is enabled
  84. func (auth *AuthProxy) IsEnabled() bool {
  85. // Bail if the setting is not enabled
  86. return auth.enabled
  87. }
  88. // HasHeader checks if the we have specified header
  89. func (auth *AuthProxy) HasHeader() bool {
  90. return len(auth.header) != 0
  91. }
  92. // IsAllowedIP compares presented IP with the whitelist one
  93. func (auth *AuthProxy) IsAllowedIP() (bool, *Error) {
  94. ip := auth.ctx.Req.RemoteAddr
  95. if len(strings.TrimSpace(auth.whitelistIP)) == 0 {
  96. return true, nil
  97. }
  98. proxies := strings.Split(auth.whitelistIP, ",")
  99. var proxyObjs []*net.IPNet
  100. for _, proxy := range proxies {
  101. result, err := coerceProxyAddress(proxy)
  102. if err != nil {
  103. return false, newError("Could not get the network", err)
  104. }
  105. proxyObjs = append(proxyObjs, result)
  106. }
  107. sourceIP, _, _ := net.SplitHostPort(ip)
  108. sourceObj := net.ParseIP(sourceIP)
  109. for _, proxyObj := range proxyObjs {
  110. if proxyObj.Contains(sourceObj) {
  111. return true, nil
  112. }
  113. }
  114. err := fmt.Errorf(
  115. "Request for user (%s) from %s is not from the authentication proxy", auth.header,
  116. sourceIP,
  117. )
  118. return false, newError("Proxy authentication required", err)
  119. }
  120. // getKey forms a key for the cache based on the headers received as part of the authentication flow.
  121. // Our configuration supports multiple headers. The main header contains the email or username.
  122. // And the additional ones that allow us to specify extra attributes: Name, Email or Groups.
  123. func (auth *AuthProxy) getKey() string {
  124. key := strings.TrimSpace(auth.header) // start the key with the main header
  125. auth.headersIterator(func(_, header string) {
  126. key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
  127. })
  128. hashedKey := base32.StdEncoding.EncodeToString([]byte(key))
  129. return fmt.Sprintf(CachePrefix, hashedKey)
  130. }
  131. // Login logs in user id with whatever means possible
  132. func (auth *AuthProxy) Login() (int64, *Error) {
  133. id, _ := auth.GetUserViaCache()
  134. if id != 0 {
  135. // Error here means absent cache - we don't need to handle that
  136. return id, nil
  137. }
  138. if isLDAPEnabled() {
  139. id, err := auth.LoginViaLDAP()
  140. if err == ldap.ErrInvalidCredentials {
  141. return 0, newError(
  142. "Proxy authentication required",
  143. ldap.ErrInvalidCredentials,
  144. )
  145. }
  146. if err != nil {
  147. return 0, newError("Failed to get the user", err)
  148. }
  149. return id, nil
  150. }
  151. id, err := auth.LoginViaHeader()
  152. if err != nil {
  153. return 0, newError(
  154. "Failed to log in as user, specified in auth proxy header",
  155. err,
  156. )
  157. }
  158. return id, nil
  159. }
  160. // GetUserViaCache gets user id from cache
  161. func (auth *AuthProxy) GetUserViaCache() (int64, error) {
  162. var (
  163. cacheKey = auth.getKey()
  164. userID, err = auth.store.Get(cacheKey)
  165. )
  166. if err != nil {
  167. return 0, err
  168. }
  169. return userID.(int64), nil
  170. }
  171. // LoginViaLDAP logs in user via LDAP request
  172. func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
  173. config, err := getLDAPConfig()
  174. if err != nil {
  175. return 0, newError("Failed to get LDAP config", nil)
  176. }
  177. extUser, _, err := newLDAP(config.Servers).User(auth.header)
  178. if err != nil {
  179. return 0, newError(err.Error(), nil)
  180. }
  181. // Have to sync grafana and LDAP user during log in
  182. upsert := &models.UpsertUserCommand{
  183. ReqContext: auth.ctx,
  184. SignupAllowed: auth.LDAPAllowSignup,
  185. ExternalUser: extUser,
  186. }
  187. err = bus.Dispatch(upsert)
  188. if err != nil {
  189. return 0, newError(err.Error(), nil)
  190. }
  191. return upsert.Result.Id, nil
  192. }
  193. // LoginViaHeader logs in user from the header only
  194. func (auth *AuthProxy) LoginViaHeader() (int64, error) {
  195. extUser := &models.ExternalUserInfo{
  196. AuthModule: "authproxy",
  197. AuthId: auth.header,
  198. }
  199. switch auth.headerType {
  200. case "username":
  201. extUser.Login = auth.header
  202. emailAddr, emailErr := mail.ParseAddress(auth.header) // only set Email if it can be parsed as an email address
  203. if emailErr == nil {
  204. extUser.Email = emailAddr.Address
  205. }
  206. case "email":
  207. extUser.Email = auth.header
  208. extUser.Login = auth.header
  209. default:
  210. return 0, newError("Auth proxy header property invalid", nil)
  211. }
  212. auth.headersIterator(func(field string, header string) {
  213. if field == "Groups" {
  214. extUser.Groups = util.SplitString(header)
  215. } else {
  216. reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
  217. }
  218. })
  219. upsert := &models.UpsertUserCommand{
  220. ReqContext: auth.ctx,
  221. SignupAllowed: setting.AuthProxyAutoSignUp,
  222. ExternalUser: extUser,
  223. }
  224. err := bus.Dispatch(upsert)
  225. if err != nil {
  226. return 0, err
  227. }
  228. return upsert.Result.Id, nil
  229. }
  230. // headersIterator iterates over all non-empty supported additional headers
  231. func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
  232. for _, field := range supportedHeaderFields {
  233. h := auth.headers[field]
  234. if h == "" {
  235. continue
  236. }
  237. if value := auth.ctx.Req.Header.Get(h); value != "" {
  238. fn(field, strings.TrimSpace(value))
  239. }
  240. }
  241. }
  242. // GetSignedUser get full signed user info
  243. func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error) {
  244. query := &models.GetSignedInUserQuery{
  245. OrgId: auth.orgID,
  246. UserId: userID,
  247. }
  248. if err := bus.Dispatch(query); err != nil {
  249. return nil, newError(err.Error(), nil)
  250. }
  251. return query.Result, nil
  252. }
  253. // Remember user in cache
  254. func (auth *AuthProxy) Remember(id int64) *Error {
  255. key := auth.getKey()
  256. // Check if user already in cache
  257. userID, _ := auth.store.Get(key)
  258. if userID != nil {
  259. return nil
  260. }
  261. expiration := time.Duration(auth.cacheTTL) * time.Minute
  262. err := auth.store.Set(key, id, expiration)
  263. if err != nil {
  264. return newError(err.Error(), nil)
  265. }
  266. return nil
  267. }
  268. // coerceProxyAddress gets network of the presented CIDR notation
  269. func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) {
  270. proxyAddr = strings.TrimSpace(proxyAddr)
  271. if !strings.Contains(proxyAddr, "/") {
  272. proxyAddr = strings.Join([]string{proxyAddr, "32"}, "/")
  273. }
  274. _, network, err := net.ParseCIDR(proxyAddr)
  275. return network, err
  276. }