mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
shared library for managing external user accounts
This commit is contained in:
@@ -101,13 +101,13 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
|
||||
return Error(401, "Login is disabled", nil)
|
||||
}
|
||||
|
||||
authQuery := login.LoginUserQuery{
|
||||
authQuery := m.LoginUserQuery{
|
||||
Username: cmd.User,
|
||||
Password: cmd.Password,
|
||||
IpAddress: c.Req.RemoteAddr,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&authQuery); err != nil {
|
||||
if err := login.AuthenticateUser(c, &authQuery); err != nil {
|
||||
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
|
||||
return Error(401, "Invalid username or password", err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -14,24 +13,16 @@ import (
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||
oauthLogger = log.New("oauth")
|
||||
)
|
||||
var oauthLogger = log.New("oauth")
|
||||
|
||||
func GenStateString() string {
|
||||
rnd := make([]byte, 32)
|
||||
@@ -56,7 +47,7 @@ func OAuthLogin(ctx *m.ReqContext) {
|
||||
if errorParam != "" {
|
||||
errorDesc := ctx.Query("error_description")
|
||||
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
|
||||
redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
|
||||
redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,54 +140,42 @@ func OAuthLogin(ctx *m.ReqContext) {
|
||||
|
||||
// validate that we got at least an email address
|
||||
if userInfo.Email == "" {
|
||||
redirectWithError(ctx, ErrNoEmail)
|
||||
redirectWithError(ctx, login.ErrNoEmail)
|
||||
return
|
||||
}
|
||||
|
||||
// validate that the email is allowed to login to grafana
|
||||
if !connect.IsEmailAllowed(userInfo.Email) {
|
||||
redirectWithError(ctx, ErrEmailNotAllowed)
|
||||
redirectWithError(ctx, login.ErrEmailNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
userQuery := m.GetUserByEmailQuery{Email: userInfo.Email}
|
||||
err = bus.Dispatch(&userQuery)
|
||||
extUser := m.ExternalUserInfo{
|
||||
AuthModule: "oauth_" + name,
|
||||
AuthId: userInfo.Id,
|
||||
Name: userInfo.Name,
|
||||
Login: userInfo.Login,
|
||||
Email: userInfo.Email,
|
||||
OrgRoles: map[int64]m.RoleType{},
|
||||
}
|
||||
|
||||
// create account if missing
|
||||
if err == m.ErrUserNotFound {
|
||||
if !connect.IsSignupAllowed() {
|
||||
redirectWithError(ctx, ErrSignUpNotAllowed)
|
||||
return
|
||||
}
|
||||
limitReached, err := quota.QuotaReached(ctx, "user")
|
||||
if err != nil {
|
||||
ctx.Handle(500, "Failed to get user quota", err)
|
||||
return
|
||||
}
|
||||
if limitReached {
|
||||
redirectWithError(ctx, ErrUsersQuotaReached)
|
||||
return
|
||||
}
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: userInfo.Login,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Company: userInfo.Company,
|
||||
DefaultOrgRole: userInfo.Role,
|
||||
}
|
||||
if userInfo.Role != "" {
|
||||
extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
|
||||
}
|
||||
|
||||
if err = bus.Dispatch(&cmd); err != nil {
|
||||
ctx.Handle(500, "Failed to create account", err)
|
||||
return
|
||||
}
|
||||
|
||||
userQuery.Result = &cmd.Result
|
||||
} else if err != nil {
|
||||
ctx.Handle(500, "Unexpected error", err)
|
||||
// add/update user in grafana
|
||||
userQuery := &m.UpsertUserCommand{
|
||||
ExternalUser: &extUser,
|
||||
SignupAllowed: connect.IsSignupAllowed(),
|
||||
}
|
||||
err = login.UpsertUser(ctx, userQuery)
|
||||
if err != nil {
|
||||
redirectWithError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// login
|
||||
loginUserWithUser(userQuery.Result, ctx)
|
||||
loginUserWithUser(userQuery.User, ctx)
|
||||
|
||||
metrics.M_Api_Login_OAuth.Inc()
|
||||
|
||||
|
||||
@@ -8,23 +8,22 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
|
||||
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
ErrGettingUserQuota = errors.New("Error getting user quota")
|
||||
)
|
||||
|
||||
type LoginUserQuery struct {
|
||||
Username string
|
||||
Password string
|
||||
User *m.User
|
||||
IpAddress string
|
||||
}
|
||||
|
||||
func Init() {
|
||||
bus.AddHandler("auth", AuthenticateUser)
|
||||
loadLdapConfig()
|
||||
}
|
||||
|
||||
func AuthenticateUser(query *LoginUserQuery) error {
|
||||
func AuthenticateUser(ctx *m.ReqContext, query *m.LoginUserQuery) error {
|
||||
if err := validateLoginAttempts(query.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -34,7 +33,7 @@ func AuthenticateUser(query *LoginUserQuery) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ldapEnabled, ldapErr := loginUsingLdap(query)
|
||||
ldapEnabled, ldapErr := loginUsingLdap(ctx, query)
|
||||
if ldapEnabled {
|
||||
if ldapErr == nil || ldapErr != ErrInvalidCredentials {
|
||||
return ldapErr
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestAuthenticateUser(t *testing.T) {
|
||||
}
|
||||
|
||||
type authScenarioContext struct {
|
||||
loginUserQuery *LoginUserQuery
|
||||
loginUserQuery *m.LoginUserQuery
|
||||
grafanaLoginWasCalled bool
|
||||
ldapLoginWasCalled bool
|
||||
loginAttemptValidationWasCalled bool
|
||||
@@ -161,14 +161,14 @@ type authScenarioContext struct {
|
||||
type authScenarioFunc func(sc *authScenarioContext)
|
||||
|
||||
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
|
||||
loginUsingGrafanaDB = func(query *LoginUserQuery) error {
|
||||
loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
|
||||
sc.grafanaLoginWasCalled = true
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
|
||||
loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
|
||||
loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
|
||||
sc.ldapLoginWasCalled = true
|
||||
return enabled, err
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
|
||||
}
|
||||
|
||||
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
|
||||
saveInvalidLoginAttempt = func(query *LoginUserQuery) {
|
||||
saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
|
||||
sc.saveInvalidLoginAttemptWasCalled = true
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func authScenario(desc string, fn authScenarioFunc) {
|
||||
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
|
||||
|
||||
sc := &authScenarioContext{
|
||||
loginUserQuery: &LoginUserQuery{
|
||||
loginUserQuery: &m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
@@ -34,7 +34,7 @@ var validateLoginAttempts = func(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var saveInvalidLoginAttempt = func(query *LoginUserQuery) {
|
||||
var saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
|
||||
if setting.DisableBruteForceLoginProtection {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
saveInvalidLoginAttempt(&LoginUserQuery{
|
||||
saveInvalidLoginAttempt(&m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
@@ -103,7 +103,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
saveInvalidLoginAttempt(&LoginUserQuery{
|
||||
saveInvalidLoginAttempt(&m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
157
pkg/login/ext_user.go
Normal file
157
pkg/login/ext_user.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
)
|
||||
|
||||
func UpsertUser(ctx *m.ReqContext, cmd *m.UpsertUserCommand) error {
|
||||
extUser := cmd.ExternalUser
|
||||
|
||||
userQuery := m.GetUserByAuthInfoQuery{
|
||||
AuthModule: extUser.AuthModule,
|
||||
AuthId: extUser.AuthId,
|
||||
UserId: extUser.UserId,
|
||||
Email: extUser.Email,
|
||||
Login: extUser.Login,
|
||||
}
|
||||
err := bus.Dispatch(&userQuery)
|
||||
if err != nil {
|
||||
if err != m.ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cmd.SignupAllowed {
|
||||
log.Warn(fmt.Sprintf("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule))
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
limitReached, err := quota.QuotaReached(ctx, "user")
|
||||
if err != nil {
|
||||
log.Warn("Error getting user quota", "err", err)
|
||||
return ErrGettingUserQuota
|
||||
}
|
||||
if limitReached {
|
||||
return ErrUsersQuotaReached
|
||||
}
|
||||
|
||||
cmd.User, err = createUser(extUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cmd.User = userQuery.User
|
||||
|
||||
// sync user info
|
||||
err = updateUser(cmd.User, extUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = syncOrgRoles(cmd.User, extUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: extUser.Login,
|
||||
Email: extUser.Email,
|
||||
Name: extUser.Name,
|
||||
}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd2 := m.SetAuthInfoCommand{
|
||||
UserId: cmd.Result.Id,
|
||||
AuthModule: extUser.AuthModule,
|
||||
AuthId: extUser.AuthId,
|
||||
}
|
||||
if err := bus.Dispatch(&cmd2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmd.Result, nil
|
||||
}
|
||||
|
||||
func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
|
||||
// sync user info
|
||||
if user.Login != extUser.Login || user.Email != extUser.Email || user.Name != extUser.Name {
|
||||
log.Debug("Syncing user info", "id", user.Id, "login", extUser.Login, "email", extUser.Email)
|
||||
updateCmd := m.UpdateUserCommand{
|
||||
UserId: user.Id,
|
||||
Login: extUser.Login,
|
||||
Email: extUser.Email,
|
||||
Name: extUser.Name,
|
||||
}
|
||||
err := bus.Dispatch(&updateCmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
|
||||
if len(extUser.OrgRoles) == 0 {
|
||||
// log.Warn("No group mappings defined")
|
||||
return nil
|
||||
}
|
||||
|
||||
orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
|
||||
if err := bus.Dispatch(&orgsQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handledOrgIds := map[int64]bool{}
|
||||
deleteOrgIds := []int64{}
|
||||
|
||||
// update existing org roles
|
||||
for _, org := range orgsQuery.Result {
|
||||
handledOrgIds[org.OrgId] = true
|
||||
|
||||
if extUser.OrgRoles[org.OrgId] == "" {
|
||||
deleteOrgIds = append(deleteOrgIds, org.OrgId)
|
||||
} else if extUser.OrgRoles[org.OrgId] != org.Role {
|
||||
// update role
|
||||
cmd := m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extUser.OrgRoles[org.OrgId]}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add any new org roles
|
||||
for orgId, orgRole := range extUser.OrgRoles {
|
||||
if _, exists := handledOrgIds[orgId]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// add role
|
||||
cmd := m.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId}
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil && err != m.ErrOrgNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete any removed org roles
|
||||
for _, orgId := range deleteOrgIds {
|
||||
cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -17,7 +17,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa
|
||||
return nil
|
||||
}
|
||||
|
||||
var loginUsingGrafanaDB = func(query *LoginUserQuery) error {
|
||||
var loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
|
||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
|
||||
|
||||
if err := bus.Dispatch(&userQuery); err != nil {
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestGrafanaLogin(t *testing.T) {
|
||||
}
|
||||
|
||||
type grafanaLoginScenarioContext struct {
|
||||
loginUserQuery *LoginUserQuery
|
||||
loginUserQuery *m.LoginUserQuery
|
||||
validatePasswordCalled bool
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) {
|
||||
origValidatePassword := validatePassword
|
||||
|
||||
sc := &grafanaLoginScenarioContext{
|
||||
loginUserQuery: &LoginUserQuery{
|
||||
loginUserQuery: &m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-ldap/ldap"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -24,10 +23,9 @@ type ILdapConn interface {
|
||||
}
|
||||
|
||||
type ILdapAuther interface {
|
||||
Login(query *LoginUserQuery) error
|
||||
SyncSignedInUser(signedInUser *m.SignedInUser) error
|
||||
GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
|
||||
SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error
|
||||
Login(ctx *m.ReqContext, query *m.LoginUserQuery) error
|
||||
SyncSignedInUser(ctx *m.ReqContext, signedInUser *m.SignedInUser) error
|
||||
GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
|
||||
}
|
||||
|
||||
type ldapAuther struct {
|
||||
@@ -89,89 +87,36 @@ func (a *ldapAuther) Dial() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *ldapAuther) Login(query *LoginUserQuery) error {
|
||||
if err := a.Dial(); err != nil {
|
||||
func (a *ldapAuther) Login(ctx *m.ReqContext, query *m.LoginUserQuery) error {
|
||||
// connect to ldap server
|
||||
err := a.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.conn.Close()
|
||||
|
||||
// perform initial authentication
|
||||
if err := a.initialBind(query.Username, query.Password); err != nil {
|
||||
err = a.initialBind(query.Username, query.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find user entry & attributes
|
||||
if ldapUser, err := a.searchForUser(query.Username); err != nil {
|
||||
return err
|
||||
} else {
|
||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||
|
||||
// check if a second user bind is needed
|
||||
if a.requireSecondBind {
|
||||
if err := a.secondBind(ldapUser, query.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if grafanaUser, err := a.GetGrafanaUserFor(ldapUser); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil {
|
||||
return syncErr
|
||||
}
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ldapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
||||
grafanaUser := m.User{
|
||||
Id: signedInUser.UserId,
|
||||
Login: signedInUser.Login,
|
||||
Email: signedInUser.Email,
|
||||
Name: signedInUser.Name,
|
||||
}
|
||||
|
||||
if err := a.Dial(); err != nil {
|
||||
ldapUser, err := a.searchForUser(query.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer a.conn.Close()
|
||||
if err := a.serverBind(); err != nil {
|
||||
return err
|
||||
}
|
||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||
|
||||
if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil {
|
||||
a.log.Error("Failed searching for user in ldap", "error", err)
|
||||
|
||||
return err
|
||||
} else {
|
||||
if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil {
|
||||
// check if a second user bind is needed
|
||||
if a.requireSecondBind {
|
||||
err = a.secondBind(ldapUser, query.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync info for ldap user and grafana user
|
||||
func (a *ldapAuther) syncInfoAndOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
||||
// sync user details
|
||||
if err := a.syncUserInfo(user, ldapUser); err != nil {
|
||||
return err
|
||||
}
|
||||
// sync org roles
|
||||
if err := a.SyncOrgRoles(user, ldapUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
// validate that the user has access
|
||||
// if there are no ldap group mappings access is true
|
||||
// otherwise a single group must match
|
||||
@@ -184,125 +129,87 @@ func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
|
||||
}
|
||||
|
||||
if !access {
|
||||
a.log.Info("Ldap Auth: user does not belong in any of the specified ldap groups", "username", ldapUser.Username, "groups", ldapUser.MemberOf)
|
||||
return nil, ErrInvalidCredentials
|
||||
a.log.Info(
|
||||
"Ldap Auth: user does not belong in any of the specified ldap groups",
|
||||
"username", ldapUser.Username,
|
||||
"groups", ldapUser.MemberOf)
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// get user from grafana db
|
||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username}
|
||||
if err := bus.Dispatch(&userQuery); err != nil {
|
||||
if err == m.ErrUserNotFound && setting.LdapAllowSignup {
|
||||
return a.createGrafanaUser(ldapUser)
|
||||
} else if err == m.ErrUserNotFound {
|
||||
a.log.Warn("Not allowing LDAP login, user not found in internal user database, and ldap allow signup = false")
|
||||
return nil, ErrInvalidCredentials
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return userQuery.Result, nil
|
||||
|
||||
}
|
||||
func (a *ldapAuther) createGrafanaUser(ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: ldapUser.Username,
|
||||
Email: ldapUser.Email,
|
||||
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmd.Result, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *LdapUserInfo) error {
|
||||
var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
|
||||
if user.Email == ldapUser.Email && user.Name == name {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.log.Debug("Syncing user info", "username", ldapUser.Username)
|
||||
updateCmd := m.UpdateUserCommand{}
|
||||
updateCmd.UserId = user.Id
|
||||
updateCmd.Login = user.Login
|
||||
updateCmd.Email = ldapUser.Email
|
||||
updateCmd.Name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
|
||||
return bus.Dispatch(&updateCmd)
|
||||
}
|
||||
|
||||
func (a *ldapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
||||
if len(a.server.LdapGroups) == 0 {
|
||||
a.log.Warn("No group mappings defined")
|
||||
return nil
|
||||
}
|
||||
|
||||
orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
|
||||
if err := bus.Dispatch(&orgsQuery); err != nil {
|
||||
grafanaUser, err := a.GetGrafanaUserFor(ctx, ldapUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handledOrgIds := map[int64]bool{}
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
|
||||
// update or remove org roles
|
||||
for _, org := range orgsQuery.Result {
|
||||
match := false
|
||||
handledOrgIds[org.OrgId] = true
|
||||
|
||||
for _, group := range a.server.LdapGroups {
|
||||
if org.OrgId != group.OrgId {
|
||||
continue
|
||||
}
|
||||
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
match = true
|
||||
if org.Role != group.OrgRole {
|
||||
// update role
|
||||
cmd := m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: group.OrgRole}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// ignore subsequent ldap group mapping matches
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// remove role if no mappings match
|
||||
if !match {
|
||||
cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
func (a *ldapAuther) SyncSignedInUser(ctx *m.ReqContext, signedInUser *m.SignedInUser) error {
|
||||
err := a.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add missing org roles
|
||||
for _, group := range a.server.LdapGroups {
|
||||
if !ldapUser.isMemberOf(group.GroupDN) {
|
||||
continue
|
||||
}
|
||||
defer a.conn.Close()
|
||||
|
||||
if _, exists := handledOrgIds[group.OrgId]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// add role
|
||||
cmd := m.AddOrgUserCommand{UserId: user.Id, Role: group.OrgRole, OrgId: group.OrgId}
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil && err != m.ErrOrgNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// mark this group has handled so we do not process it again
|
||||
handledOrgIds[group.OrgId] = true
|
||||
err = a.serverBind()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ldapUser, err := a.searchForUser(signedInUser.Login)
|
||||
if err != nil {
|
||||
a.log.Error("Failed searching for user in ldap", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
grafanaUser, err := a.GetGrafanaUserFor(ctx, ldapUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedInUser.Login = grafanaUser.Login
|
||||
signedInUser.Email = grafanaUser.Email
|
||||
signedInUser.Name = grafanaUser.Name
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
extUser := m.ExternalUserInfo{
|
||||
AuthModule: "ldap",
|
||||
AuthId: ldapUser.DN,
|
||||
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
|
||||
Login: ldapUser.Username,
|
||||
Email: ldapUser.Email,
|
||||
OrgRoles: map[int64]m.RoleType{},
|
||||
}
|
||||
|
||||
for _, group := range a.server.LdapGroups {
|
||||
// only use the first match for each org
|
||||
if extUser.OrgRoles[group.OrgId] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
}
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
userQuery := m.UpsertUserCommand{
|
||||
ExternalUser: &extUser,
|
||||
SignupAllowed: setting.LdapAllowSignup,
|
||||
}
|
||||
err := UpsertUser(ctx, &userQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userQuery.User, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) serverBind() error {
|
||||
// bind_dn and bind_password to bind
|
||||
if err := a.conn.Bind(a.server.BindDN, a.server.BindPassword); err != nil {
|
||||
@@ -470,7 +377,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func createUserFromLdapInfo() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
|
||||
var loginUsingLdap = func(ctx *m.ReqContext, query *m.LoginUserQuery) (bool, error) {
|
||||
if !setting.LdapEnabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, server := range LdapCfg.Servers {
|
||||
author := NewLdapAuthenticator(server)
|
||||
err := author.Login(query)
|
||||
err := author.Login(ctx, query)
|
||||
if err == nil || err != ErrInvalidCredentials {
|
||||
return true, err
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestLdapLogin(t *testing.T) {
|
||||
|
||||
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
|
||||
sc.withLoginResult(false)
|
||||
enabled, err := loginUsingLdap(&LoginUserQuery{
|
||||
enabled, err := loginUsingLdap(&m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
})
|
||||
@@ -117,7 +117,7 @@ type mockLdapAuther struct {
|
||||
loginCalled bool
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
|
||||
func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error {
|
||||
a.loginCalled = true
|
||||
|
||||
if !a.validLogin {
|
||||
@@ -140,7 +140,7 @@ func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) erro
|
||||
}
|
||||
|
||||
type ldapLoginScenarioContext struct {
|
||||
loginUserQuery *LoginUserQuery
|
||||
loginUserQuery *m.LoginUserQuery
|
||||
ldapAuthenticatorMock *mockLdapAuther
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
|
||||
origNewLdapAuthenticator := NewLdapAuthenticator
|
||||
|
||||
sc := &ldapLoginScenarioContext{
|
||||
loginUserQuery: &LoginUserQuery{
|
||||
loginUserQuery: &m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
@@ -112,7 +112,7 @@ var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUs
|
||||
|
||||
for _, server := range ldapCfg.Servers {
|
||||
author := login.NewLdapAuthenticator(server)
|
||||
if err := author.SyncSignedInUser(query.Result); err != nil {
|
||||
if err := author.SyncSignedInUser(ctx, query.Result); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ type mockLdapAuthenticator struct {
|
||||
syncSignedInUserCalled bool
|
||||
}
|
||||
|
||||
func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error {
|
||||
func (a *mockLdapAuthenticator) Login(query *m.LoginUserQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
l "github.com/grafana/grafana/pkg/login"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -165,7 +164,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
||||
|
||||
user := loginQuery.Result
|
||||
|
||||
loginUserQuery := l.LoginUserQuery{Username: username, Password: password, User: user}
|
||||
loginUserQuery := m.LoginUserQuery{Username: username, Password: password, User: user}
|
||||
if err := bus.Dispatch(&loginUserQuery); err != nil {
|
||||
ctx.JsonApiErr(401, "Invalid username or password", err)
|
||||
return true
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
ms "github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
l "github.com/grafana/grafana/pkg/login"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -72,7 +71,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(loginUserQuery *l.LoginUserQuery) error {
|
||||
bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
66
pkg/models/user_auth.go
Normal file
66
pkg/models/user_auth.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package models
|
||||
|
||||
type UserAuth struct {
|
||||
Id int64
|
||||
UserId int64
|
||||
AuthModule string
|
||||
AuthId string
|
||||
}
|
||||
|
||||
type ExternalUserInfo struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
OrgRoles map[int64]RoleType
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
|
||||
type UpsertUserCommand struct {
|
||||
ExternalUser *ExternalUserInfo
|
||||
SignupAllowed bool
|
||||
|
||||
User *User
|
||||
}
|
||||
|
||||
type SetAuthInfoCommand struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
}
|
||||
|
||||
type DeleteAuthInfoCommand struct {
|
||||
UserAuth *UserAuth
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// QUERIES
|
||||
|
||||
type LoginUserQuery struct {
|
||||
Username string
|
||||
Password string
|
||||
User *User
|
||||
IpAddress string
|
||||
}
|
||||
|
||||
type GetUserByAuthInfoQuery struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
Email string
|
||||
Login string
|
||||
|
||||
User *User
|
||||
UserAuth *UserAuth
|
||||
}
|
||||
|
||||
type GetAuthInfoQuery struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
|
||||
UserAuth *UserAuth
|
||||
}
|
||||
@@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addDashboardAclMigrations(mg)
|
||||
addTagMigration(mg)
|
||||
addLoginAttemptMigrations(mg)
|
||||
addUserAuthMigrations(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
||||
24
pkg/services/sqlstore/migrations/user_auth_mig.go
Normal file
24
pkg/services/sqlstore/migrations/user_auth_mig.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addUserAuthMigrations(mg *Migrator) {
|
||||
userAuthV1 := Table{
|
||||
Name: "user_auth",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "user_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "auth_module", Type: DB_NVarchar, Length: 30, Nullable: false},
|
||||
{Name: "auth_id", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"auth_module", "auth_id"}},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1))
|
||||
// add indices
|
||||
addTableIndicesMigrations(mg, "v1", userAuthV1)
|
||||
}
|
||||
@@ -445,6 +445,7 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
|
||||
"DELETE FROM dashboard_acl WHERE user_id = ?",
|
||||
"DELETE FROM preferences WHERE user_id = ?",
|
||||
"DELETE FROM team_member WHERE user_id = ?",
|
||||
"DELETE FROM user_auth WHERE user_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
|
||||
130
pkg/services/sqlstore/user_auth.go
Normal file
130
pkg/services/sqlstore/user_auth.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetUserByAuthInfo)
|
||||
bus.AddHandler("sql", GetAuthInfo)
|
||||
bus.AddHandler("sql", SetAuthInfo)
|
||||
bus.AddHandler("sql", DeleteAuthInfo)
|
||||
}
|
||||
|
||||
func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
|
||||
user := new(m.User)
|
||||
has := false
|
||||
var err error
|
||||
|
||||
// Try to find the user by auth module and id first
|
||||
if query.AuthModule != "" && query.AuthId != "" {
|
||||
authQuery := &m.GetAuthInfoQuery{
|
||||
AuthModule: query.AuthModule,
|
||||
AuthId: query.AuthId,
|
||||
}
|
||||
|
||||
err = GetAuthInfo(authQuery)
|
||||
// if user id was specified and doesn't match the user_auth entry, remove it
|
||||
if err == nil && query.UserId != 0 && query.UserId != authQuery.UserAuth.UserId {
|
||||
DeleteAuthInfo(&m.DeleteAuthInfoCommand{
|
||||
UserAuth: authQuery.UserAuth,
|
||||
})
|
||||
} else if err == nil {
|
||||
has, err = x.Id(authQuery.UserAuth.UserId).Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
query.UserAuth = authQuery.UserAuth
|
||||
} else {
|
||||
// if the user has been deleted then remove the entry
|
||||
DeleteAuthInfo(&m.DeleteAuthInfoCommand{
|
||||
UserAuth: authQuery.UserAuth,
|
||||
})
|
||||
}
|
||||
} else if err != m.ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try to find the user by id
|
||||
if !has && query.UserId != 0 {
|
||||
has, err = x.Id(query.UserId).Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try to find the user by email address
|
||||
if !has && query.Email != "" {
|
||||
user = &m.User{Email: query.Email}
|
||||
has, err = x.Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try to find the user by login
|
||||
if !has && query.Login != "" {
|
||||
user = &m.User{Login: query.Login}
|
||||
has, err = x.Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// No user found
|
||||
if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
query.User = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAuthInfo(query *m.GetAuthInfoQuery) error {
|
||||
userAuth := &m.UserAuth{
|
||||
AuthModule: query.AuthModule,
|
||||
AuthId: query.AuthId,
|
||||
}
|
||||
has, err := x.Get(userAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
query.UserAuth = userAuth
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetAuthInfo(cmd *m.SetAuthInfoCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
authUser := m.UserAuth{
|
||||
UserId: cmd.UserId,
|
||||
AuthModule: cmd.AuthModule,
|
||||
AuthId: cmd.AuthId,
|
||||
}
|
||||
|
||||
_, err := sess.Insert(&authUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
_, err := sess.Delete(cmd.UserAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -51,6 +51,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
|
||||
|
||||
func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
@@ -69,6 +70,7 @@ func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Id: fmt.Sprintf("%d", data.Id),
|
||||
Name: data.Name,
|
||||
Login: data.Login,
|
||||
Email: data.Email,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
type BasicUserInfo struct {
|
||||
Id string
|
||||
Name string
|
||||
Email string
|
||||
Login string
|
||||
|
||||
Reference in New Issue
Block a user