diff --git a/conf/sample.ini b/conf/sample.ini index c77752438ebd5..bbde0469db4a4 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -276,6 +276,13 @@ ;editor_roles = _member_ ;read_editor_roles = ;viewer_roles = +;verify_ssl_cert = true +;root_ca_pem_file = /etc/grafana/Keystone_CA.crt +# Whether to store keystone password in a cookie (true) or in a session variable (false) +;cookie_credentials = true +# Encryption key for storing keystone password (empty = no encryption) +# AES key should be 32 bytes +;credential_aes_key = 123456789,123456789,123456789,12 #################################### SMTP / Emailing ########################## [smtp] diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index ef94e26f0d449..4c516552a7fac 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/cloudwatch" "github.com/grafana/grafana/pkg/api/keystone" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -55,6 +56,8 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht // clear cookie headers req.Header.Del("Cookie") req.Header.Del("Set-Cookie") + + log.Info("Proxying call to %s", req.URL.String()) } return &httputil.ReverseProxy{Director: director, FlushInterval: time.Millisecond * 200} diff --git a/pkg/api/keystone/keystone.go b/pkg/api/keystone/keystone.go index 7b62588c88427..f9b352db7e64a 100644 --- a/pkg/api/keystone/keystone.go +++ b/pkg/api/keystone/keystone.go @@ -3,10 +3,18 @@ package keystone import ( "time" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "io" + "strings" ) const ( @@ -17,7 +25,18 @@ const ( ) func getUserName(c *middleware.Context) (string, error) { - userQuery := m.GetUserByIdQuery{Id: c.Session.Get(middleware.SESS_KEY_USERID).(int64)} + var keystoneUserIdObj interface{} + if setting.KeystoneCookieCredentials { + if keystoneUserIdObj = c.GetCookie(setting.CookieUserName); keystoneUserIdObj == nil { + return "", errors.New("Couldn't find cookie containing keystone userId") + } else { + return keystoneUserIdObj.(string), nil + } + } else if keystoneUserIdObj = c.Session.Get(middleware.SESS_KEY_USERID); keystoneUserIdObj == nil { + return "", errors.New("Session timed out trying to get keystone userId") + } + + userQuery := m.GetUserByIdQuery{Id: keystoneUserIdObj.(int64)} if err := bus.Dispatch(&userQuery); err != nil { if err == m.ErrUserNotFound { return "", err @@ -46,14 +65,41 @@ func getNewToken(c *middleware.Context) (string, error) { return "", err } + var keystonePasswordObj interface{} + if setting.KeystoneCookieCredentials { + if keystonePasswordObj = c.GetCookie(middleware.SESS_KEY_PASSWORD); keystonePasswordObj == nil { + return "", errors.New("Couldn't find cookie containing keystone password") + } else { + log.Debug("Got password from cookie") + } + } else if keystonePasswordObj = c.Session.Get(middleware.SESS_KEY_PASSWORD); keystonePasswordObj == nil { + return "", errors.New("Session timed out trying to get keystone password") + } else if keystonePasswordObj != nil { + log.Debug("Got password from session") + } + + if setting.KeystoneCredentialAesKey != "" { + keystonePasswordObj = decryptPassword(keystonePasswordObj.(string)) + log.Debug("Decrypted password") + } else { + log.Warn("Password stored in cleartext!") + } + + user, domain := UserDomain(username) + // Remove @domain from project name + keystoneProject := strings.Replace(project, "@"+domain, "", 1) auth := Auth_data{ - Username: username, - Project: project, - Password: c.Session.Get(middleware.SESS_KEY_PASSWORD).(string), - Domain: setting.KeystoneDefaultDomain, + Username: user, + Project: keystoneProject, + Password: keystonePasswordObj.(string), + Domain: domain, Server: setting.KeystoneURL, } if err := AuthenticateScoped(&auth); err != nil { + c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) + c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) + c.SetCookie(middleware.SESS_KEY_PASSWORD, "", -1, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) + c.Session.Destory(c) return "", err } @@ -108,3 +154,52 @@ func GetToken(c *middleware.Context) (string, error) { } return token, nil } + +func EncryptPassword(password string) string { + key := []byte(setting.KeystoneCredentialAesKey) + block, err := aes.NewCipher(key) + if err != nil { + log.Error(3, "Error: NewCipher(%d bytes) = %s", len(setting.KeystoneCredentialAesKey), err) + } + ciphertext := make([]byte, aes.BlockSize+len(password)) + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + log.Error(3, "Error: %s", err) + } + stream := cipher.NewOFB(block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(password)) + + return base64.StdEncoding.EncodeToString(ciphertext) +} + +func decryptPassword(base64ciphertext string) string { + key := []byte(setting.KeystoneCredentialAesKey) + block, err := aes.NewCipher(key) + if err != nil { + log.Error(3, "Error: NewCipher(%d bytes) = %s", len(setting.KeystoneCredentialAesKey), err) + } + ciphertext, err := base64.StdEncoding.DecodeString(base64ciphertext) + if err != nil { + log.Error(3, "Error: %s", err) + return "" + } + if aes.BlockSize > len(ciphertext) { + log.Error(3, "Error: ciphertext %s is shorter than AES blocksize %d", ciphertext, aes.BlockSize) + return "" + } + iv := ciphertext[:aes.BlockSize] + password := make([]byte, len(ciphertext)-aes.BlockSize) + stream := cipher.NewOFB(block, iv) + stream.XORKeyStream(password, ciphertext[aes.BlockSize:]) + return string(password) +} + +func UserDomain(username string) (string, string) { + user := username + domain := setting.KeystoneDefaultDomain + if at_idx := strings.IndexRune(username, '@'); at_idx > 0 { + domain = username[at_idx+1:] + user = username[:at_idx] + } + return user, domain +} diff --git a/pkg/api/keystone/keystone_requests.go b/pkg/api/keystone/keystone_requests.go index 458e4b80a4633..f71a7f12201a2 100644 --- a/pkg/api/keystone/keystone_requests.go +++ b/pkg/api/keystone/keystone_requests.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/json" "errors" + "fmt" + "github.com/grafana/grafana/pkg/log" "net/http" ) @@ -88,8 +90,9 @@ type auth_response_struct struct { } type auth_token_struct struct { - Roles []auth_roles_struct `json:"roles"` - Expires_at string `json:"expires_at"` + Roles []auth_roles_struct `json:"roles"` + Expires_at string `json:"expires_at"` + User auth_user_response_struct `json:"user"` } type auth_roles_struct struct { @@ -97,14 +100,26 @@ type auth_roles_struct struct { Name string `json:"name"` } +type auth_user_response_struct struct { + Name string `json:"name"` + Id string `json:"id"` + Domain auth_userdomain_response_struct `json:"domain"` +} + +type auth_userdomain_response_struct struct { + Name string `json:"name"` + Id string `json:"id"` +} + // Projects Response type project_response_struct struct { Projects []project_struct } type project_struct struct { - Name string - Enabled bool + Name string + Enabled bool + DomainId string `json:"domain_id"` } //////////////////////// @@ -115,6 +130,7 @@ type project_struct struct { type Auth_data struct { Server string Domain string + DomainId string Username string Password string Project string @@ -191,14 +207,17 @@ func authenticate(data *Auth_data, b []byte) error { data.Token = resp.Header.Get("X-Subject-Token") data.Expiration = auth_response.Token.Expires_at data.Roles = auth_response.Token.Roles + data.DomainId = auth_response.Token.User.Domain.Id + data.Username = auth_response.Token.User.Name return nil } // Projects Section type Projects_data struct { - Token string - Server string + Token string + Server string + DomainId string //response Projects []string } @@ -232,7 +251,7 @@ func GetProjects(data *Projects_data) error { return err } for _, project := range project_response.Projects { - if project.Enabled { + if project.Enabled && (project.DomainId == data.DomainId) { data.Projects = append(data.Projects, project.Name) } } diff --git a/pkg/api/login.go b/pkg/api/login.go index 76bee051cf01e..e79fcdce686b9 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -4,6 +4,7 @@ import ( "net/url" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/api/keystone" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" @@ -112,7 +113,21 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response { loginUserWithUser(user, c) if setting.KeystoneEnabled { - c.Session.Set(middleware.SESS_KEY_PASSWORD, cmd.Password) + if setting.KeystoneCredentialAesKey != "" { + cmd.Password = keystone.EncryptPassword(cmd.Password) + } + if setting.KeystoneCookieCredentials { + log.Debug("c.Req.Header.Get(\"X-Forwarded-Proto\"): %s", c.Req.Header.Get("X-Forwarded-Proto")) + var days interface{} + if setting.LogInRememberDays == 0 { + days = nil + } else { + days = 86400 * setting.LogInRememberDays + } + c.SetCookie(middleware.SESS_KEY_PASSWORD, cmd.Password, days, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) + } else { + c.Session.Set(middleware.SESS_KEY_PASSWORD, cmd.Password) + } } result := map[string]interface{}{ @@ -136,16 +151,18 @@ func loginUserWithUser(user *m.User, c *middleware.Context) { days := 86400 * setting.LogInRememberDays if days > 0 { - c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/") - c.SetSuperSecureCookie(util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/") + c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) + c.SetSuperSecureCookie(util.EncodeMd5(user.Rands+user.Password), + setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) } c.Session.Set(middleware.SESS_KEY_USERID, user.Id) } func Logout(c *middleware.Context) { - c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/") - c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/") + c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) + c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) + c.SetCookie(middleware.SESS_KEY_PASSWORD, "", -1, setting.AppSubUrl+"/", nil, middleware.IsSecure(c), true) c.Session.Destory(c) c.Redirect(setting.AppSubUrl + "/login") } diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 7380a65d810ae..3f0a5410fd8ab 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -4,6 +4,7 @@ import ( "errors" "crypto/subtle" + "github.com/grafana/grafana/pkg/api/keystone" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -42,8 +43,12 @@ func AuthenticateUser(query *LoginUserQuery) error { } if setting.KeystoneEnabled { + user, domain := keystone.UserDomain(query.Username) + if domain == setting.KeystoneDefaultDomain { + query.Username = user + } auther := NewKeystoneAuthenticator(setting.KeystoneURL, - setting.KeystoneDefaultDomain, + domain, setting.KeystoneGlobalAdminRoles, setting.KeystoneAdminRoles, setting.KeystoneEditorRoles, diff --git a/pkg/login/keystone.go b/pkg/login/keystone.go index b6d2855939d1e..99cb236b1b3f2 100644 --- a/pkg/login/keystone.go +++ b/pkg/login/keystone.go @@ -6,11 +6,15 @@ import ( "github.com/grafana/grafana/pkg/api/keystone" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "strings" + "github.com/grafana/grafana/pkg/log" ) type keystoneAuther struct { server string domainname string + domainId string + defaultrole string roles map[m.RoleType][]string admin_roles []string @@ -32,7 +36,7 @@ func NewKeystoneAuthenticator(server, domainname string, global_admin_roles, adm func (a *keystoneAuther) login(query *LoginUserQuery) error { // perform initial authentication - if err := a.authenticate(query.Username, query.Password); err != nil { + if err := a.authenticate(query); err != nil { return err } @@ -49,17 +53,26 @@ func (a *keystoneAuther) login(query *LoginUserQuery) error { } -func (a *keystoneAuther) authenticate(username, password string) error { +func (a *keystoneAuther) authenticate(query *LoginUserQuery) error { + user, _ := keystone.UserDomain(query.Username) auth := keystone.Auth_data{ Server: a.server, - Username: username, - Password: password, + Username: user, + Password: query.Password, Domain: a.domainname, } if err := keystone.AuthenticateUnscoped(&auth); err != nil { return err } a.token = auth.Token + a.domainId = auth.DomainId + + // Make sure we store the username with the same case as Keystone + // in case the actual username is a different case + userNameDomain := strings.Split(query.Username, "@") + userNameDomain[0] = auth.Username + query.Username = strings.Join(userNameDomain, "@") + return nil } @@ -103,10 +116,14 @@ func (a *keystoneAuther) updateGrafanaUserPermissions(userid int64, isAdmin bool } func (a *keystoneAuther) getGrafanaOrgFor(orgname string) (*m.Org, error) { + + log.Debug("getGrafanaOrgFor( %v )", orgname) + // get org from grafana db orgQuery := m.GetOrgByNameQuery{Name: orgname} if err := bus.Dispatch(&orgQuery); err != nil { if err == m.ErrOrgNotFound { + log.Debug("orgname %s not found - create it", orgname) return a.createGrafanaOrg(orgname) } else { return nil, err @@ -200,6 +217,7 @@ func (a *keystoneAuther) syncOrgRoles(username, password string, user *m.User) e // add missing org roles for project, _ := range a.project_list { if grafanaOrg, err := a.getGrafanaOrgFor(project); err != nil { + log.Error(3, "Couldn't find Grafana org %s", project) return err } else { if _, exists := handledOrgIds[grafanaOrg.Id]; exists { @@ -275,9 +293,11 @@ func (a *keystoneAuther) syncOrgRoles(username, password string, user *m.User) e } func (a *keystoneAuther) getProjectList(username, password string) error { + log.Trace("getProjectList() with username %s", username) projects_data := keystone.Projects_data{ - Token: a.token, - Server: a.server, + Token: a.token, + Server: a.server, + DomainId: a.domainId, } if err := keystone.GetProjects(&projects_data); err != nil { return err @@ -297,12 +317,13 @@ func (a *keystoneAuther) getProjectList(username, password string) error { for _, role := range auth.Roles { roles = append(roles, role.Name) } - a.project_list[project] = roles + a.project_list[project+"@"+a.domainname] = roles } return nil } func (a *keystoneAuther) getRole(user_roles []string) m.RoleType { + log.Trace("getRole(%v)", user_roles) role_map := make(map[string]bool) for _, role := range user_roles { role_map[role] = true diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 4b59fada62e4d..ad1284e00fb04 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -238,3 +238,7 @@ func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool { func (ctx *Context) TimeRequest(timer metrics.Timer) { ctx.Data["perfmon.timer"] = timer } + +func IsSecure(ctx *Context) bool { + return (ctx.Req.TLS != nil) || (ctx.Req.Header.Get("X-Forwarded-Proto") == "https") +} diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 8501d08d25c35..1189858ba3df9 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -12,11 +12,9 @@ import ( ) const ( - SESS_KEY_USERID = "uid" - SESS_KEY_OAUTH_STATE = "state" - SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys - SESS_KEY_LASTLDAPSYNC = "last_ldap_sync" - SESS_KEY_PASSWORD = "password" + SESS_KEY_USERID = "uid" + SESS_KEY_OAUTH_STATE = "state" + SESS_KEY_PASSWORD = "grafana_password" ) var sessionManager *session.Manager diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f32d85654d850..9b4d19b95efda 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -142,14 +142,19 @@ var ( LdapAllowSignup bool = true // Keystone - KeystoneEnabled bool - KeystoneURL string - KeystoneDefaultDomain string - KeystoneViewerRoles []string - KeystoneReadEditorRoles []string - KeystoneEditorRoles []string - KeystoneAdminRoles []string - KeystoneGlobalAdminRoles []string + KeystoneEnabled bool + KeystoneCookieCredentials bool + KeystoneCredentialAesKey string + KeystoneURL string + KeystoneDefaultDomain string + KeystoneDefaultRole string + KeystoneViewerRoles []string + KeystoneReadEditorRoles []string + KeystoneEditorRoles []string + KeystoneAdminRoles []string + KeystoneGlobalAdminRoles []string + KeystoneVerifySSLCert bool + KeystoneRootCAPEMFile string // SMTP email settings Smtp SmtpSettings @@ -585,13 +590,8 @@ func NewConfigContext(args *CommandLineArgs) error { keystone := Cfg.Section("auth.keystone") KeystoneEnabled = keystone.Key("enabled").MustBool(false) - KeystoneURL = keystone.Key("auth_url").String() - KeystoneV3 = keystone.Key("v3").MustBool(false) - - // SSL - - keystone := Cfg.Section("auth.keystone") - KeystoneEnabled = keystone.Key("enabled").MustBool(false) + KeystoneCookieCredentials = keystone.Key("cookie_credentials").MustBool(false) + KeystoneCredentialAesKey = keystone.Key("credential_aes_key").String() KeystoneURL = keystone.Key("auth_url").String() KeystoneDefaultDomain = keystone.Key("default_domain").String() KeystoneViewerRoles = strings.Split(keystone.Key("viewer_roles").String(), ",")