Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions conf/sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@
;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]
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/dataproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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"
Expand Down Expand Up @@ -67,6 +68,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}
Expand Down
93 changes: 88 additions & 5 deletions pkg/api/keystone/keystone.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +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 (
Expand All @@ -19,7 +26,13 @@ const (

func getUserName(c *middleware.Context) (string, error) {
var keystoneUserIdObj interface{}
if keystoneUserIdObj = c.Session.Get(middleware.SESS_KEY_USERID); keystoneUserIdObj == nil {
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")
}

Expand Down Expand Up @@ -53,18 +66,39 @@ func getNewToken(c *middleware.Context) (string, error) {
}

var keystonePasswordObj interface{}
if keystonePasswordObj = c.Session.Get(middleware.SESS_KEY_PASSWORD); keystonePasswordObj == nil {
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)
keystoneProject := strings.Replace(project, "@"+domain, "", 1)
auth := Auth_data{
Username: username,
Project: project,
Username: user,
Project: keystoneProject,
Password: keystonePasswordObj.(string),
Domain: setting.KeystoneDefaultDomain,
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
}

Expand Down Expand Up @@ -124,3 +158,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 ""
}
iv := ciphertext[:aes.BlockSize]
if aes.BlockSize > len(ciphertext) {
log.Error(3, "Error: ciphertext %s is shorter than AES blocksize %d", ciphertext, aes.BlockSize)
return ""
}
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
}
30 changes: 23 additions & 7 deletions pkg/api/keystone/keystone_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,23 +93,36 @@ 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 {
Id string `json:"id"`
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"`
}

////////////////////////
Expand All @@ -120,6 +133,7 @@ type project_struct struct {
type Auth_data struct {
Server string
Domain string
DomainId string
Username string
Password string
Project string
Expand Down Expand Up @@ -205,6 +219,7 @@ 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

return nil
}
Expand All @@ -225,8 +240,9 @@ func anonymisePasswordsTokens(data *Auth_data, json []byte) []byte {

// Projects Section
type Projects_data struct {
Token string
Server string
Token string
Server string
DomainId string
//response
Projects []string
}
Expand Down Expand Up @@ -264,7 +280,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)
}
}
Expand Down
27 changes: 22 additions & 5 deletions pkg/api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{}{
Expand All @@ -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")
}
7 changes: 6 additions & 1 deletion pkg/login/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.KeystoneDefaultRole,
setting.KeystoneGlobalAdminRoles,
setting.KeystoneAdminRoles,
Expand Down
19 changes: 15 additions & 4 deletions pkg/login/keystone.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
type keystoneAuther struct {
server string
domainname string
domainId string
defaultrole string
roles map[m.RoleType][]string
admin_roles []string
Expand Down Expand Up @@ -56,16 +57,18 @@ func (a *keystoneAuther) login(query *LoginUserQuery) error {
}

func (a *keystoneAuther) authenticate(username, password string) error {
user, _ := keystone.UserDomain(username)
auth := keystone.Auth_data{
Server: a.server,
Username: username,
Username: user,
Password: password,
Domain: a.domainname,
}
if err := keystone.AuthenticateUnscoped(&auth); err != nil {
return err
}
a.token = auth.Token
a.domainId = auth.DomainId
return nil
}

Expand Down Expand Up @@ -109,10 +112,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
Expand Down Expand Up @@ -209,6 +216,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 {
Expand Down Expand Up @@ -284,9 +292,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
Expand All @@ -306,12 +316,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
Expand Down
Loading