| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- package authproxy
- import (
- "encoding/base32"
- "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/services/multildap"
- "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 = ldap.IsEnabled
- // newLDAP creates multiple LDAP instance
- var newLDAP = multildap.New
- // supportedHeaders states the supported headers configuration fields
- var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"}
- // AuthProxy struct
- type AuthProxy struct {
- store *remotecache.RemoteCache
- ctx *models.ReqContext
- orgID int64
- header string
- enabled bool
- LDAPAllowSignup bool
- AuthProxyAutoSignUp 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 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,
- enabled: setting.AuthProxyEnabled,
- headerType: setting.AuthProxyHeaderProperty,
- headers: setting.AuthProxyHeaders,
- whitelistIP: setting.AuthProxyWhitelist,
- cacheTTL: setting.AuthProxyLDAPSyncTtl,
- LDAPAllowSignup: setting.LDAPAllowSignup,
- AuthProxyAutoSignUp: setting.AuthProxyAutoSignUp,
- }
- }
- // IsEnabled checks if the proxy auth is enabled
- func (auth *AuthProxy) IsEnabled() bool {
- // Bail if the setting is not enabled
- return auth.enabled
- }
- // HasHeader checks if the we have specified header
- func (auth *AuthProxy) HasHeader() bool {
- return len(auth.header) != 0
- }
- // 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)
- }
- // 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 or Groups.
- func (auth *AuthProxy) getKey() string {
- key := strings.TrimSpace(auth.header) // start the key with the main header
- auth.headersIterator(func(_, header string) {
- key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
- })
- hashedKey := base32.StdEncoding.EncodeToString([]byte(key))
- return fmt.Sprintf(CachePrefix, hashedKey)
- }
- // Login logs in user id with whatever means possible
- func (auth *AuthProxy) Login() (int64, *Error) {
- id, _ := auth.GetUserViaCache()
- if id != 0 {
- // Error here means absent cache - we don't need to handle that
- return id, nil
- }
- if isLDAPEnabled() {
- id, err := auth.LoginViaLDAP()
- if err == ldap.ErrInvalidCredentials {
- return 0, newError(
- "Proxy authentication required",
- ldap.ErrInvalidCredentials,
- )
- }
- if err != nil {
- return 0, newError("Failed to get the user", err)
- }
- return id, nil
- }
- id, err := auth.LoginViaHeader()
- 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() (int64, error) {
- var (
- cacheKey = auth.getKey()
- userID, err = auth.store.Get(cacheKey)
- )
- if err != nil {
- return 0, err
- }
- return userID.(int64), nil
- }
- // LoginViaLDAP logs in user via LDAP request
- func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
- config, err := getLDAPConfig()
- if err != nil {
- return 0, newError("Failed to get LDAP config", nil)
- }
- extUser, _, err := newLDAP(config.Servers).User(auth.header)
- if err != nil {
- return 0, newError(err.Error(), nil)
- }
- // Have to sync grafana and LDAP user during log in
- upsert := &models.UpsertUserCommand{
- ReqContext: auth.ctx,
- SignupAllowed: auth.LDAPAllowSignup,
- ExternalUser: extUser,
- }
- err = bus.Dispatch(upsert)
- if err != nil {
- return 0, newError(err.Error(), nil)
- }
- return upsert.Result.Id, nil
- }
- // LoginViaHeader logs in user from the header only
- func (auth *AuthProxy) LoginViaHeader() (int64, error) {
- extUser := &models.ExternalUserInfo{
- AuthModule: "authproxy",
- AuthId: auth.header,
- }
- switch auth.headerType {
- case "username":
- extUser.Login = auth.header
- emailAddr, emailErr := mail.ParseAddress(auth.header) // only set Email if it can be parsed as an email address
- if emailErr == nil {
- extUser.Email = emailAddr.Address
- }
- case "email":
- extUser.Email = auth.header
- extUser.Login = auth.header
- default:
- return 0, newError("Auth proxy header property invalid", nil)
- }
- auth.headersIterator(func(field string, header string) {
- if field == "Groups" {
- extUser.Groups = util.SplitString(header)
- } else {
- reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
- }
- })
- upsert := &models.UpsertUserCommand{
- ReqContext: auth.ctx,
- SignupAllowed: setting.AuthProxyAutoSignUp,
- ExternalUser: extUser,
- }
- err := bus.Dispatch(upsert)
- if err != nil {
- return 0, err
- }
- return upsert.Result.Id, nil
- }
- // headersIterator iterates over all non-empty supported additional headers
- func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
- for _, field := range supportedHeaderFields {
- h := auth.headers[field]
- if h == "" {
- continue
- }
- if value := auth.ctx.Req.Header.Get(h); value != "" {
- fn(field, strings.TrimSpace(value))
- }
- }
- }
- // 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(id int64) *Error {
- key := auth.getKey()
- // Check if user already in cache
- userID, _ := auth.store.Get(key)
- if userID != nil {
- return nil
- }
- expiration := time.Duration(auth.cacheTTL) * time.Minute
- err := auth.store.Set(key, id, 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
- }
|