Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4 Case sensitive login #9

Open
wants to merge 6 commits into
base: master-keystone
Choose a base branch
from
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
94 changes: 89 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,40 @@ 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)
// Remove @domain from project name
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 +159,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
}
31 changes: 24 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,8 @@ 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
}
Expand All @@ -225,8 +241,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 +281,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
Loading