Skip to content

Commit

Permalink
Squashed set of Darren's commits
Browse files Browse the repository at this point in the history
Log the URL when calling a datasource from the proxy

Fixes twc-openstack#5

Configuration options:
cookie_credentials - set to true to use cookie instead of session for storing Keystone password
credential_aes_key - 32-char encryption key. If set, this key is used to encrypt/decrypt the stored Keystone password

Multi-domain login

Fix: allow users with cross-domain role assignments to log in

(cherry picked from commit 66316d7)

Case sensitive login fix

If the backend Keystone is case-insensitive (perhaps using AD for authentication)
then we could end up with several users in Grafana, one for each combination of
 upper & lowercase chars in the username. This fix always uses the username returned
 in the Keystone response as the username for Grafana, regardless of the case used
 in the login screen.

(cherry picked from commit 1b8b6f9)

Fix bug in "Case sensitive login fix"

(cherry picked from commit 27a5f06)

Formatting updates - "go fmt" applied

Check length of ciphertext *before* decoding it

(cherry picked from commit 0e54407)
  • Loading branch information
dhague authored and dougszumski committed Dec 5, 2018
1 parent 9118fb5 commit 3116dd3
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 45 deletions.
7 changes: 7 additions & 0 deletions conf/sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/dataproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}
Expand Down
105 changes: 100 additions & 5 deletions pkg/api/keystone/keystone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
33 changes: 26 additions & 7 deletions pkg/api/keystone/keystone_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/log"
"net/http"
)

Expand Down Expand Up @@ -88,23 +90,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 @@ -115,6 +130,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 @@ -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
}
Expand Down Expand Up @@ -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)
}
}
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.KeystoneGlobalAdminRoles,
setting.KeystoneAdminRoles,
setting.KeystoneEditorRoles,
Expand Down
Loading

0 comments on commit 3116dd3

Please sign in to comment.