Explorar o código

Merge branch 'master' into docs-5.1

Marcus Efraimsson %!s(int64=7) %!d(string=hai) anos
pai
achega
7e8bd8d004
Modificáronse 84 ficheiros con 1427 adicións e 601 borrados
  1. 2 2
      .bra.toml
  2. 20 0
      .circleci/config.yml
  3. 3 0
      CHANGELOG.md
  4. 22 9
      build.go
  5. 2 2
      docs/sources/features/datasources/elasticsearch.md
  6. 2 2
      docs/sources/features/datasources/influxdb.md
  7. 2 2
      docs/sources/http_api/data_source.md
  8. 2 5
      pkg/api/avatar/avatar.go
  9. 1 1
      pkg/api/http_server.go
  10. 6 5
      pkg/api/login.go
  11. 27 46
      pkg/api/login_oauth.go
  12. 1 1
      pkg/cmd/grafana-cli/commands/install_command.go
  13. 1 1
      pkg/cmd/grafana-cli/commands/ls_command.go
  14. 1 2
      pkg/cmd/grafana-cli/commands/upgrade_all_command.go
  15. 0 7
      pkg/components/dynmap/dynmap.go
  16. 4 4
      pkg/components/dynmap/dynmap_test.go
  17. 1 4
      pkg/log/file.go
  18. 9 10
      pkg/login/auth.go
  19. 5 5
      pkg/login/auth_test.go
  20. 1 1
      pkg/login/brute_force_login_protection.go
  21. 2 2
      pkg/login/brute_force_login_protection_test.go
  22. 184 0
      pkg/login/ext_user.go
  23. 1 1
      pkg/login/grafana_login.go
  24. 2 2
      pkg/login/grafana_login_test.go
  25. 68 174
      pkg/login/ldap.go
  26. 2 1
      pkg/login/ldap_login.go
  27. 6 10
      pkg/login/ldap_login_test.go
  28. 62 31
      pkg/login/ldap_test.go
  29. 1 5
      pkg/metrics/graphitebridge/graphite.go
  30. 95 63
      pkg/middleware/auth_proxy.go
  31. 36 24
      pkg/middleware/auth_proxy_test.go
  32. 1 2
      pkg/middleware/middleware.go
  33. 27 5
      pkg/middleware/middleware_test.go
  34. 2 2
      pkg/models/org_user.go
  35. 72 0
      pkg/models/user_auth.go
  36. 1 1
      pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go
  37. 3 3
      pkg/services/alerting/conditions/evaluator.go
  38. 1 1
      pkg/services/alerting/conditions/query.go
  39. 1 1
      pkg/services/alerting/extractor.go
  40. 1 1
      pkg/services/alerting/notifiers/telegram.go
  41. 2 2
      pkg/services/alerting/rule.go
  42. 2 2
      pkg/services/alerting/scheduler.go
  43. 1 2
      pkg/services/notifications/mailer.go
  44. 1 5
      pkg/services/provisioning/provisioning.go
  45. 1 6
      pkg/services/sqlstore/alert_notification.go
  46. 2 5
      pkg/services/sqlstore/annotation.go
  47. 2 2
      pkg/services/sqlstore/apikey.go
  48. 8 18
      pkg/services/sqlstore/dashboard.go
  49. 2 4
      pkg/services/sqlstore/dashboard_acl.go
  50. 1 1
      pkg/services/sqlstore/dashboard_snapshot.go
  51. 1 0
      pkg/services/sqlstore/migrations/migrations.go
  52. 24 0
      pkg/services/sqlstore/migrations/user_auth_mig.go
  53. 2 4
      pkg/services/sqlstore/migrator/dialect.go
  54. 1 2
      pkg/services/sqlstore/migrator/migrations.go
  55. 1 1
      pkg/services/sqlstore/migrator/types.go
  56. 1 1
      pkg/services/sqlstore/plugin_setting.go
  57. 4 4
      pkg/services/sqlstore/quota.go
  58. 0 4
      pkg/services/sqlstore/stats.go
  59. 1 5
      pkg/services/sqlstore/team.go
  60. 1 1
      pkg/services/sqlstore/temp_user.go
  61. 17 24
      pkg/services/sqlstore/user.go
  62. 148 0
      pkg/services/sqlstore/user_auth.go
  63. 131 0
      pkg/services/sqlstore/user_auth_test.go
  64. 1 1
      pkg/social/generic_oauth.go
  65. 2 0
      pkg/social/grafana_com_oauth.go
  66. 1 0
      pkg/social/social.go
  67. 0 3
      pkg/tsdb/cloudwatch/cloudwatch.go
  68. 4 15
      pkg/tsdb/cloudwatch/metric_find_query.go
  69. 1 4
      pkg/tsdb/cloudwatch/metric_find_query_test.go
  70. 5 5
      pkg/tsdb/mssql/mssql.go
  71. 4 4
      pkg/tsdb/mysql/mysql.go
  72. 5 5
      pkg/tsdb/postgres/postgres.go
  73. 1 1
      pkg/tsdb/sql_engine.go
  74. 1 5
      pkg/util/shortid_generator.go
  75. 4 0
      public/app/core/components/ScrollBar/ScrollBar.tsx
  76. 1 1
      public/app/core/components/sidemenu/sidemenu.html
  77. 134 9
      public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
  78. 102 0
      public/app/features/dashboard/specs/AddPanelPanel.jest.tsx
  79. 4 4
      public/app/features/panel/panel_ctrl.ts
  80. 3 3
      public/app/plugins/datasource/graphite/datasource.ts
  81. 95 0
      public/app/plugins/datasource/graphite/specs/datasource_specs.ts
  82. 19 2
      public/sass/components/_panel_add_panel.scss
  83. 5 7
      public/sass/components/_tabs.scss
  84. 1 1
      public/sass/components/_tags.scss

+ 2 - 2
.bra.toml

@@ -1,6 +1,6 @@
 [run]
 init_cmds = [
-  ["go", "run", "build.go", "build-server"],
+  ["go", "run", "build.go", "-dev", "build-server"],
 	["./bin/grafana-server", "cfg:app_mode=development"]
 ]
 watch_all = true
@@ -12,6 +12,6 @@ watch_dirs = [
 watch_exts = [".go", ".ini", ".toml"]
 build_delay = 1500
 cmds = [
-  ["go", "run", "build.go", "build"],
+  ["go", "run", "build.go", "-dev", "build"],
 	["./bin/grafana-server", "cfg:app_mode=development"]
 ]

+ 20 - 0
.circleci/config.yml

@@ -1,6 +1,22 @@
 version: 2
 
 jobs:
+  codespell:
+    docker:
+      - image: circleci/python
+    steps:
+      - checkout
+      - run:
+          name: install codespell
+          command: 'sudo pip install codespell'
+      - run:
+          # Important: all words have to be in lowercase, and separated by "\n".
+          name: exclude known exceptions
+          command: 'echo -e "unknwon" > words_to_ignore.txt'
+      - run:
+          name: check documentation spelling errors
+          command: 'codespell -I ./words_to_ignore.txt docs/'
+
   test-frontend:
     docker:
       - image: circleci/node:6.11.4
@@ -103,6 +119,10 @@ workflows:
   version: 2
   test-and-build:
     jobs:
+      - codespell:
+          filters:
+            tags:
+              only: /.*/
       - build:
           filters:
             tags:

+ 3 - 0
CHANGELOG.md

@@ -54,9 +54,12 @@
 * **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm)
 * **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747)
 * **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572)
+* **Dashboard**: Add search filter/tabs to new panel control [#10427](https://github.com/grafana/grafana/issues/10427)
 * **Folders**: User with org viewer role should not be able to save/move dashboards in/to general folder [#11553](https://github.com/grafana/grafana/issues/11553)
 
 ### Tech
+* Backend code simplification [#11613](https://github.com/grafana/grafana/pull/11613), thx [@knweiss](https://github.com/knweiss)
+* Add codespell to CI [#11602](https://github.com/grafana/grafana/pull/11602), thx [@mjtrangoni](https://github.com/mjtrangoni)
 * Migrated JavaScript files to TypeScript
 
 # 5.0.4 (2018-03-28)

+ 22 - 9
build.go

@@ -41,6 +41,7 @@ var (
 	includeBuildNumber    bool     = true
 	buildNumber           int      = 0
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
+	isDev                 bool     = false
 )
 
 const minGoVersion = 1.8
@@ -61,6 +62,7 @@ func main() {
 	flag.BoolVar(&race, "race", race, "Use race detector")
 	flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
 	flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
+	flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
 	flag.Parse()
 
 	readVersionFromPackageJson()
@@ -394,7 +396,9 @@ func build(binaryName, pkg string, tags []string) {
 		binary += ".exe"
 	}
 
-	rmr(binary, binary+".md5")
+	if !isDev {
+		rmr(binary, binary+".md5")
+	}
 	args := []string{"build", "-ldflags", ldflags()}
 	if len(tags) > 0 {
 		args = append(args, "-tags", strings.Join(tags, ","))
@@ -405,16 +409,21 @@ func build(binaryName, pkg string, tags []string) {
 
 	args = append(args, "-o", binary)
 	args = append(args, pkg)
-	setBuildEnv()
 
-	runPrint("go", "version")
+	if !isDev {
+		setBuildEnv()
+		runPrint("go", "version")
+	}
+
 	runPrint("go", args...)
 
-	// Create an md5 checksum of the binary, to be included in the archive for
-	// automatic upgrades.
-	err := md5File(binary)
-	if err != nil {
-		log.Fatal(err)
+	if !isDev {
+		// Create an md5 checksum of the binary, to be included in the archive for
+		// automatic upgrades.
+		err := md5File(binary)
+		if err != nil {
+			log.Fatal(err)
+		}
 	}
 }
 
@@ -435,6 +444,10 @@ func rmr(paths ...string) {
 }
 
 func clean() {
+	if isDev {
+		return
+	}
+
 	rmr("dist")
 	rmr("tmp")
 	rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/grafana", goos, goarch)))
@@ -550,7 +563,7 @@ func shaFilesInDist() {
 			return nil
 		}
 
-		if strings.Contains(path, ".sha256") == false {
+		if !strings.Contains(path, ".sha256") {
 			err := shaFile(path)
 			if err != nil {
 				log.Printf("Failed to create sha file. error: %v\n", err)

+ 2 - 2
docs/sources/features/datasources/elasticsearch.md

@@ -63,7 +63,7 @@ are supported.
 
 ### Min time interval
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
-This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
 number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
 
 Identifier | Description
@@ -178,4 +178,4 @@ datasources:
     jsonData:
       interval: Daily
       timeField: "@timestamp"
-```
+```

+ 2 - 2
docs/sources/features/datasources/influxdb.md

@@ -45,7 +45,7 @@ All requests will be made from the browser directly to the data source and may b
 
 ### Min time interval
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
-This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
 number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
 
 Identifier | Description
@@ -212,4 +212,4 @@ datasources:
     user: grafana
     password: grafana
     url: http://localhost:8086
-```
+```

+ 2 - 2
docs/sources/http_api/data_source.md

@@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
     "defaultRegion": "us-west-1"
   },
   "secureJsonData": {
-    "accessKey": "Ol4pIDpeKSA6XikgOl4p",
-    "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
+    "accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
+    "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
   }
 }
 ```

+ 2 - 5
pkg/api/avatar/avatar.go

@@ -258,9 +258,6 @@ func (this *thunderTask) fetch() error {
 	this.Avatar.data = &bytes.Buffer{}
 	writer := bufio.NewWriter(this.Avatar.data)
 
-	if _, err = io.Copy(writer, resp.Body); err != nil {
-		return err
-	}
-
-	return nil
+	_, err = io.Copy(writer, resp.Body)
+	return err
 }

+ 1 - 1
pkg/api/http_server.go

@@ -139,7 +139,7 @@ func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
 	}
 
 	hs.httpSrv.TLSConfig = tlsCfg
-	hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
+	hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
 
 	return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
 }

+ 6 - 5
pkg/api/login.go

@@ -101,13 +101,14 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 		return Error(401, "Login is disabled", nil)
 	}
 
-	authQuery := login.LoginUserQuery{
-		Username:  cmd.User,
-		Password:  cmd.Password,
-		IpAddress: c.Req.RemoteAddr,
+	authQuery := &m.LoginUserQuery{
+		ReqContext: c,
+		Username:   cmd.User,
+		Password:   cmd.Password,
+		IpAddress:  c.Req.RemoteAddr,
 	}
 
-	if err := bus.Dispatch(&authQuery); err != nil {
+	if err := bus.Dispatch(authQuery); err != nil {
 		if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
 			return Error(401, "Invalid username or password", err)
 		}

+ 27 - 46
pkg/api/login_oauth.go

@@ -6,7 +6,6 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
-	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -16,22 +15,15 @@ import (
 
 	"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 +48,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 +141,43 @@ 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)
-
-	// 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,
-		}
+	extUser := &m.ExternalUserInfo{
+		AuthModule: "oauth_" + name,
+		AuthId:     userInfo.Id,
+		Name:       userInfo.Name,
+		Login:      userInfo.Login,
+		Email:      userInfo.Email,
+		OrgRoles:   map[int64]m.RoleType{},
+	}
 
-		if err = bus.Dispatch(&cmd); err != nil {
-			ctx.Handle(500, "Failed to create account", err)
-			return
-		}
+	if userInfo.Role != "" {
+		extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
+	}
 
-		userQuery.Result = &cmd.Result
-	} else if err != nil {
-		ctx.Handle(500, "Unexpected error", err)
+	// add/update user in grafana
+	cmd := &m.UpsertUserCommand{
+		ReqContext:    ctx,
+		ExternalUser:  extUser,
+		SignupAllowed: connect.IsSignupAllowed(),
+	}
+	err = bus.Dispatch(cmd)
+	if err != nil {
+		redirectWithError(ctx, err)
+		return
 	}
 
 	// login
-	loginUserWithUser(userQuery.Result, ctx)
+	loginUserWithUser(cmd.Result, ctx)
 
 	metrics.M_Api_Login_OAuth.Inc()
 

+ 1 - 1
pkg/cmd/grafana-cli/commands/install_command.go

@@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
 	fileInfo, err := os.Stat(pluginsDir)
 	if err != nil {
 		if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
-			return errors.New(fmt.Sprintf("pluginsDir (%s) is not a writable directory", pluginsDir))
+			return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
 		}
 		return nil
 	}

+ 1 - 1
pkg/cmd/grafana-cli/commands/ls_command.go

@@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
 		return fmt.Errorf("error: %s", err)
 	}
 
-	if pluginDirInfo.IsDir() == false {
+	if !pluginDirInfo.IsDir() {
 		return errors.New("plugin path is not a directory")
 	}
 

+ 1 - 2
pkg/cmd/grafana-cli/commands/upgrade_all_command.go

@@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
 	for _, p := range pluginsToUpgrade {
 		logger.Infof("Updating %v \n", p.Id)
 
-		var err error
-		err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
+		err := s.RemoveInstalledPlugin(pluginsDir, p.Id)
 		if err != nil {
 			return err
 		}

+ 0 - 7
pkg/components/dynmap/dynmap.go

@@ -585,7 +585,6 @@ func (v *Value) Null() error {
 	switch v.data.(type) {
 	case nil:
 		valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
-		break
 	}
 
 	if valid {
@@ -607,7 +606,6 @@ func (v *Value) Array() ([]*Value, error) {
 	switch v.data.(type) {
 	case []interface{}:
 		valid = true
-		break
 	}
 
 	// Unsure if this is a good way to use slices, it's probably not
@@ -638,7 +636,6 @@ func (v *Value) Number() (json.Number, error) {
 	switch v.data.(type) {
 	case json.Number:
 		valid = true
-		break
 	}
 
 	if valid {
@@ -687,7 +684,6 @@ func (v *Value) Boolean() (bool, error) {
 	switch v.data.(type) {
 	case bool:
 		valid = true
-		break
 	}
 
 	if valid {
@@ -709,7 +705,6 @@ func (v *Value) Object() (*Object, error) {
 	switch v.data.(type) {
 	case map[string]interface{}:
 		valid = true
-		break
 	}
 
 	if valid {
@@ -746,7 +741,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
 	switch v.data.(type) {
 	case []interface{}:
 		valid = true
-		break
 	}
 
 	// Unsure if this is a good way to use slices, it's probably not
@@ -782,7 +776,6 @@ func (v *Value) String() (string, error) {
 	switch v.data.(type) {
 	case string:
 		valid = true
-		break
 	}
 
 	if valid {

+ 4 - 4
pkg/components/dynmap/dynmap_test.go

@@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
 }
 
 func (assert *Assert) True(value bool, message string) {
-	if value == false {
+	if !value {
 		log.Panicln("Assert: ", message)
 	}
 }
@@ -119,13 +119,13 @@ func TestFirst(t *testing.T) {
 	assert.True(s == "" && err != nil, "nonexistent string fail")
 
 	b, err := j.GetBoolean("true")
-	assert.True(b == true && err == nil, "bool true test")
+	assert.True(b && err == nil, "bool true test")
 
 	b, err = j.GetBoolean("false")
-	assert.True(b == false && err == nil, "bool false test")
+	assert.True(!b && err == nil, "bool false test")
 
 	b, err = j.GetBoolean("invalid_field")
-	assert.True(b == false && err != nil, "bool invalid test")
+	assert.True(!b && err != nil, "bool invalid test")
 
 	list, err := j.GetValueArray("list")
 	assert.True(list != nil && err == nil, "list should be an array")

+ 1 - 4
pkg/log/file.go

@@ -99,10 +99,7 @@ func (w *FileLogWriter) StartLogger() error {
 		return err
 	}
 	w.mw.SetFd(fd)
-	if err = w.initFd(); err != nil {
-		return err
-	}
-	return nil
+	return w.initFd()
 }
 
 func (w *FileLogWriter) docheck(size int) {

+ 9 - 10
pkg/login/auth.go

@@ -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(query *m.LoginUserQuery) error {
 	if err := validateLoginAttempts(query.Username); err != nil {
 		return err
 	}

+ 5 - 5
pkg/login/auth_test.go

@@ -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",

+ 1 - 1
pkg/login/brute_force_login_protection.go

@@ -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
 	}

+ 2 - 2
pkg/login/brute_force_login_protection_test.go

@@ -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",

+ 184 - 0
pkg/login/ext_user.go

@@ -0,0 +1,184 @@
+package login
+
+import (
+	"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 init() {
+	bus.AddHandler("auth", UpsertUser)
+}
+
+func UpsertUser(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 != m.ErrUserNotFound && err != nil {
+		return err
+	}
+
+	if err != nil {
+		if !cmd.SignupAllowed {
+			log.Warn("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule)
+			return ErrInvalidCredentials
+		}
+
+		limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
+		if err != nil {
+			log.Warn("Error getting user quota", "err", err)
+			return ErrGettingUserQuota
+		}
+		if limitReached {
+			return ErrUsersQuotaReached
+		}
+
+		cmd.Result, err = createUser(extUser)
+		if err != nil {
+			return err
+		}
+
+		if extUser.AuthModule != "" && extUser.AuthId != "" {
+			cmd2 := &m.SetAuthInfoCommand{
+				UserId:     cmd.Result.Id,
+				AuthModule: extUser.AuthModule,
+				AuthId:     extUser.AuthId,
+			}
+			if err := bus.Dispatch(cmd2); err != nil {
+				return err
+			}
+		}
+
+	} else {
+		cmd.Result = userQuery.Result
+
+		err = updateUser(cmd.Result, extUser)
+		if err != nil {
+			return err
+		}
+	}
+
+	return syncOrgRoles(cmd.Result, extUser)
+}
+
+func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
+	cmd := &m.CreateUserCommand{
+		Login:        extUser.Login,
+		Email:        extUser.Email,
+		Name:         extUser.Name,
+		SkipOrgSetup: len(extUser.OrgRoles) > 0,
+	}
+	if err := bus.Dispatch(cmd); err != nil {
+		return nil, err
+	}
+
+	return &cmd.Result, nil
+}
+
+func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
+	// sync user info
+	updateCmd := &m.UpdateUserCommand{
+		UserId: user.Id,
+	}
+
+	needsUpdate := false
+	if extUser.Login != "" && extUser.Login != user.Login {
+		updateCmd.Login = extUser.Login
+		user.Login = extUser.Login
+		needsUpdate = true
+	}
+
+	if extUser.Email != "" && extUser.Email != user.Email {
+		updateCmd.Email = extUser.Email
+		user.Email = extUser.Email
+		needsUpdate = true
+	}
+
+	if extUser.Name != "" && extUser.Name != user.Name {
+		updateCmd.Name = extUser.Name
+		user.Name = extUser.Name
+		needsUpdate = true
+	}
+
+	if !needsUpdate {
+		return nil
+	}
+
+	log.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
+	return bus.Dispatch(updateCmd)
+}
+
+func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
+	// don't sync org roles if none are specified
+	if len(extUser.OrgRoles) == 0 {
+		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
+		}
+	}
+
+	// update user's default org if needed
+	if _, ok := extUser.OrgRoles[user.OrgId]; !ok {
+		for orgId := range extUser.OrgRoles {
+			user.OrgId = orgId
+			break
+		}
+
+		return bus.Dispatch(&m.SetUsingOrgCommand{
+			UserId: user.Id,
+			OrgId:  user.OrgId,
+		})
+	}
+
+	return nil
+}

+ 1 - 1
pkg/login/grafana_login.go

@@ -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 {

+ 2 - 2
pkg/login/grafana_login_test.go

@@ -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",

+ 68 - 174
pkg/login/ldap.go

@@ -24,10 +24,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(query *m.LoginUserQuery) error
+	SyncUser(query *m.LoginUserQuery) error
+	GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
 }
 
 type ldapAuther struct {
@@ -89,7 +88,8 @@ func (a *ldapAuther) Dial() error {
 	return err
 }
 
-func (a *ldapAuther) Login(query *LoginUserQuery) error {
+func (a *ldapAuther) Login(query *m.LoginUserQuery) error {
+	// connect to ldap server
 	if err := a.Dial(); err != nil {
 		return err
 	}
@@ -101,206 +101,105 @@ func (a *ldapAuther) Login(query *LoginUserQuery) error {
 	}
 
 	// find user entry & attributes
-	if ldapUser, err := a.searchForUser(query.Username); err != nil {
+	ldapUser, err := a.searchForUser(query.Username)
+	if 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
-			}
-		}
+	a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
 
-		if grafanaUser, err := a.GetGrafanaUserFor(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
-		} 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,
+	grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
+	if err != nil {
+		return err
 	}
 
-	if err := a.Dial(); err != nil {
+	query.User = grafanaUser
+	return nil
+}
+
+func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error {
+	// connect to ldap server
+	err := a.Dial()
+	if err != nil {
 		return err
 	}
-
 	defer a.conn.Close()
-	if err := a.serverBind(); err != nil {
+
+	err = a.serverBind()
+	if err != nil {
 		return err
 	}
 
-	if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil {
+	// find user entry & attributes
+	ldapUser, err := a.searchForUser(query.Username)
+	if err != nil {
 		a.log.Error("Failed searching for user in ldap", "error", err)
-
 		return err
-	} else {
-		if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil {
-			return err
-		}
-
-		a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser))
 	}
 
-	return nil
-}
+	a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
 
-// 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 {
+	grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
+	if err != nil {
 		return err
 	}
 
+	query.User = grafanaUser
 	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
-	access := len(a.server.LdapGroups) == 0
-	for _, ldapGroup := range a.server.LdapGroups {
-		if ldapUser.isMemberOf(ldapGroup.GroupDN) {
-			access = true
-			break
-		}
-	}
-
-	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
-	}
-
-	// 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
+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{},
 	}
 
-	orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
-	if err := bus.Dispatch(&orgsQuery); err != nil {
-		return err
-	}
-
-	handledOrgIds := map[int64]bool{}
-
-	// 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
-			}
-		}
-	}
-
-	// add missing org roles
 	for _, group := range a.server.LdapGroups {
-		if !ldapUser.isMemberOf(group.GroupDN) {
+		// only use the first match for each org
+		if extUser.OrgRoles[group.OrgId] != "" {
 			continue
 		}
 
-		if _, exists := handledOrgIds[group.OrgId]; exists {
-			continue
+		if ldapUser.isMemberOf(group.GroupDN) {
+			extUser.OrgRoles[group.OrgId] = group.OrgRole
 		}
+	}
 
-		// 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
-		}
+	// validate that the user has access
+	// if there are no ldap group mappings access is true
+	// otherwise a single group must match
+	if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 {
+		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
+	}
 
-		// mark this group has handled so we do not process it again
-		handledOrgIds[group.OrgId] = true
+	// add/update user in grafana
+	userQuery := &m.UpsertUserCommand{
+		ReqContext:    ctx,
+		ExternalUser:  extUser,
+		SignupAllowed: setting.LdapAllowSignup,
+	}
+	err := bus.Dispatch(userQuery)
+	if err != nil {
+		return nil, err
 	}
 
-	return nil
+	return userQuery.Result, nil
 }
 
 func (a *ldapAuther) serverBind() error {
@@ -403,8 +302,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
 		// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
 		var groupSearchResult *ldap.SearchResult
 		for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
-			var filter_replace string
-			filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
+			filter_replace := getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
 			if a.server.GroupSearchFilterUserAttribute == "" {
 				filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
 			}
@@ -470,7 +368,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
 	}
 	return []string{}
 }
-
-func createUserFromLdapInfo() error {
-	return nil
-}

+ 2 - 1
pkg/login/ldap_login.go

@@ -1,10 +1,11 @@
 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(query *m.LoginUserQuery) (bool, error) {
 	if !setting.LdapEnabled {
 		return false, nil
 	}

+ 6 - 10
pkg/login/ldap_login_test.go

@@ -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 {
@@ -127,20 +127,16 @@ func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
 	return nil
 }
 
-func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
+func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error {
 	return nil
 }
 
-func (a *mockLdapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
+func (a *mockLdapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
 	return nil, nil
 }
 
-func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
-	return nil
-}
-
 type ldapLoginScenarioContext struct {
-	loginUserQuery        *LoginUserQuery
+	loginUserQuery        *m.LoginUserQuery
 	ldapAuthenticatorMock *mockLdapAuther
 }
 
@@ -151,7 +147,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",

+ 62 - 31
pkg/login/ldap_test.go

@@ -18,7 +18,7 @@ func TestLdapAuther(t *testing.T) {
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 				LdapGroups: []*LdapGroupToOrgRole{{}},
 			})
-			_, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
 
 			So(err, ShouldEqual, ErrInvalidCredentials)
 		})
@@ -34,7 +34,7 @@ func TestLdapAuther(t *testing.T) {
 
 			sc.userQueryReturns(user1)
 
-			result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
+			result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
 			So(err, ShouldBeNil)
 			So(result, ShouldEqual, user1)
 		})
@@ -48,7 +48,7 @@ func TestLdapAuther(t *testing.T) {
 
 			sc.userQueryReturns(user1)
 
-			result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{MemberOf: []string{"cn=users"}})
+			result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"cn=users"}})
 			So(err, ShouldBeNil)
 			So(result, ShouldEqual, user1)
 		})
@@ -64,7 +64,8 @@ func TestLdapAuther(t *testing.T) {
 
 			sc.userQueryReturns(nil)
 
-			result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{
+			result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
+				DN:       "torkelo",
 				Username: "torkelo",
 				Email:    "my@email.com",
 				MemberOf: []string{"cn=editor"},
@@ -72,11 +73,6 @@ func TestLdapAuther(t *testing.T) {
 
 			So(err, ShouldBeNil)
 
-			Convey("Should create new user", func() {
-				So(sc.createUserCmd.Login, ShouldEqual, "torkelo")
-				So(sc.createUserCmd.Email, ShouldEqual, "my@email.com")
-			})
-
 			Convey("Should return new user", func() {
 				So(result.Login, ShouldEqual, "torkelo")
 			})
@@ -95,7 +91,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
-			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
 				MemberOf: []string{"cn=users"},
 			})
 
@@ -114,7 +110,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
-			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
 				MemberOf: []string{"cn=users"},
 			})
 
@@ -122,24 +118,29 @@ func TestLdapAuther(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(sc.updateOrgUserCmd, ShouldNotBeNil)
 				So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
+				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 			})
 		})
 
 		ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 				LdapGroups: []*LdapGroupToOrgRole{
-					{GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
+					{GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
 				},
 			})
 
-			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
-			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
-				MemberOf: []string{"cn=other"},
+			sc.userOrgsQueryReturns([]*m.UserOrgDTO{
+				{OrgId: 1, Role: m.ROLE_EDITOR},
+				{OrgId: 2, Role: m.ROLE_EDITOR},
+			})
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
+				MemberOf: []string{"cn=users"},
 			})
 
 			Convey("Should remove org role", func() {
 				So(err, ShouldBeNil)
 				So(sc.removeOrgUserCmd, ShouldNotBeNil)
+				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
 			})
 		})
 
@@ -152,7 +153,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
-			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
 				MemberOf: []string{"cn=users"},
 			})
 
@@ -160,6 +161,7 @@ func TestLdapAuther(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(sc.removeOrgUserCmd, ShouldBeNil)
 				So(sc.updateOrgUserCmd, ShouldNotBeNil)
+				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 			})
 		})
 
@@ -172,13 +174,14 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
-			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
 				MemberOf: []string{"cn=admins"},
 			})
 
 			Convey("Should take first match, and ignore subsequent matches", func() {
 				So(err, ShouldBeNil)
 				So(sc.updateOrgUserCmd, ShouldBeNil)
+				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 			})
 		})
 
@@ -191,19 +194,20 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
-			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
 				MemberOf: []string{"cn=admins"},
 			})
 
 			Convey("Should take first match, and ignore subsequent matches", func() {
 				So(err, ShouldBeNil)
 				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
+				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 			})
 		})
 
 	})
 
-	Convey("When calling SyncSignedInUser", t, func() {
+	Convey("When calling SyncUser", t, func() {
 
 		mockLdapConnection := &mockLdapConn{}
 		ldapAuther := NewLdapAuthenticator(
@@ -243,17 +247,20 @@ func TestLdapAuther(t *testing.T) {
 
 		ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
 			// arrange
-			signedInUser := &m.SignedInUser{
-				Email:  "roel@test.net",
-				UserId: 1,
-				Name:   "Roel Gerrits",
-				Login:  "roelgerrits",
+			query := &m.LoginUserQuery{
+				Username: "roelgerrits",
 			}
 
+			sc.userQueryReturns(&m.User{
+				Id:    1,
+				Email: "roel@test.net",
+				Name:  "Roel Gerrits",
+				Login: "roelgerrits",
+			})
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
 
 			// act
-			syncErrResult := ldapAuther.SyncSignedInUser(signedInUser)
+			syncErrResult := ldapAuther.SyncUser(query)
 
 			// assert
 			So(dialCalled, ShouldBeTrue)
@@ -299,6 +306,19 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 
 		sc := &scenarioContext{}
 
+		bus.AddHandler("test", UpsertUser)
+
+		bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
+			sc.getUserByAuthInfoQuery = cmd
+			sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
+			return nil
+		})
+
+		bus.AddHandler("test", func(cmd *m.GetUserOrgListQuery) error {
+			sc.getUserOrgListQuery = cmd
+			return nil
+		})
+
 		bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
 			sc.createUserCmd = cmd
 			sc.createUserCmd.Result = m.User{Login: cmd.Login}
@@ -325,20 +345,28 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 			return nil
 		})
 
+		bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error {
+			sc.setUsingOrgCmd = cmd
+			return nil
+		})
+
 		fn(sc)
 	})
 }
 
 type scenarioContext struct {
-	createUserCmd    *m.CreateUserCommand
-	addOrgUserCmd    *m.AddOrgUserCommand
-	updateOrgUserCmd *m.UpdateOrgUserCommand
-	removeOrgUserCmd *m.RemoveOrgUserCommand
-	updateUserCmd    *m.UpdateUserCommand
+	getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
+	getUserOrgListQuery    *m.GetUserOrgListQuery
+	createUserCmd          *m.CreateUserCommand
+	addOrgUserCmd          *m.AddOrgUserCommand
+	updateOrgUserCmd       *m.UpdateOrgUserCommand
+	removeOrgUserCmd       *m.RemoveOrgUserCommand
+	updateUserCmd          *m.UpdateUserCommand
+	setUsingOrgCmd         *m.SetUsingOrgCommand
 }
 
 func (sc *scenarioContext) userQueryReturns(user *m.User) {
-	bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
+	bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error {
 		if user == nil {
 			return m.ErrUserNotFound
 		} else {
@@ -346,6 +374,9 @@ func (sc *scenarioContext) userQueryReturns(user *m.User) {
 			return nil
 		}
 	})
+	bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error {
+		return nil
+	})
 }
 
 func (sc *scenarioContext) userOrgsQueryReturns(orgs []*m.UserOrgDTO) {

+ 1 - 5
pkg/metrics/graphitebridge/graphite.go

@@ -295,11 +295,7 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error
 		}
 	}
 
-	if err = addExtentionConventionForRollups(buf, mf, m); err != nil {
-		return err
-	}
-
-	return nil
+	return addExtentionConventionForRollups(buf, mf, m)
 }
 
 func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {

+ 95 - 63
pkg/middleware/auth_proxy.go

@@ -3,6 +3,7 @@ package middleware
 import (
 	"fmt"
 	"net"
+	"net/mail"
 	"strings"
 	"time"
 
@@ -14,6 +15,8 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 )
 
+var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+
 func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 	if !setting.AuthProxyEnabled {
 		return false
@@ -30,38 +33,102 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 		return true
 	}
 
-	query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
-	query.OrgId = orgID
-	if err := bus.Dispatch(query); err != nil {
-		if err != m.ErrUserNotFound {
-			ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
-			return true
+	// initialize session
+	if err := ctx.Session.Start(ctx.Context); err != nil {
+		log.Error(3, "Failed to start session", err)
+		return false
+	}
+
+	query := &m.GetSignedInUserQuery{OrgId: orgID}
+
+	// if this session has already been authenticated by authProxy just load the user
+	sessProxyValue := ctx.Session.Get(AUTH_PROXY_SESSION_VAR)
+	if sessProxyValue != nil && sessProxyValue.(string) == proxyHeaderValue && getRequestUserId(ctx) > 0 {
+		// if we're using ldap, sync user periodically
+		if setting.LdapEnabled {
+			syncQuery := &m.LoginUserQuery{
+				ReqContext: ctx,
+				Username:   proxyHeaderValue,
+			}
+
+			if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
+				if err == login.ErrInvalidCredentials {
+					ctx.Handle(500, "Unable to authenticate user", err)
+					return false
+				}
+
+				ctx.Handle(500, "Failed to sync user", err)
+				return false
+			}
+		}
+
+		query.UserId = getRequestUserId(ctx)
+		// if we're using ldap, pass authproxy login name to ldap user sync
+	} else if setting.LdapEnabled {
+		ctx.Session.Delete(session.SESS_KEY_LASTLDAPSYNC)
+
+		syncQuery := &m.LoginUserQuery{
+			ReqContext: ctx,
+			Username:   proxyHeaderValue,
 		}
 
-		if !setting.AuthProxyAutoSignUp {
+		if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
+			if err == login.ErrInvalidCredentials {
+				ctx.Handle(500, "Unable to authenticate user", err)
+				return false
+			}
+
+			ctx.Handle(500, "Failed to sync user", err)
 			return false
 		}
 
-		cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
-		if setting.LdapEnabled {
-			cmd.SkipOrgSetup = true
+		if syncQuery.User == nil {
+			ctx.Handle(500, "Failed to sync user", nil)
+			return false
+		}
+
+		query.UserId = syncQuery.User.Id
+		// no ldap, just use the info we have
+	} else {
+		extUser := &m.ExternalUserInfo{
+			AuthModule: "authproxy",
+			AuthId:     proxyHeaderValue,
 		}
 
-		if err := bus.Dispatch(cmd); err != nil {
-			ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
+		if setting.AuthProxyHeaderProperty == "username" {
+			extUser.Login = proxyHeaderValue
+
+			// only set Email if it can be parsed as an email address
+			emailAddr, emailErr := mail.ParseAddress(proxyHeaderValue)
+			if emailErr == nil {
+				extUser.Email = emailAddr.Address
+			}
+		} else if setting.AuthProxyHeaderProperty == "email" {
+			extUser.Email = proxyHeaderValue
+			extUser.Login = proxyHeaderValue
+		} else {
+			ctx.Handle(500, "Auth proxy header property invalid", nil)
 			return true
 		}
-		query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgID}
-		if err := bus.Dispatch(query); err != nil {
-			ctx.Handle(500, "Failed find user after creation", err)
+
+		// add/update user in grafana
+		cmd := &m.UpsertUserCommand{
+			ReqContext:    ctx,
+			ExternalUser:  extUser,
+			SignupAllowed: setting.AuthProxyAutoSignUp,
+		}
+		err := bus.Dispatch(cmd)
+		if err != nil {
+			ctx.Handle(500, "Failed to login as user specified in auth proxy header", err)
 			return true
 		}
+
+		query.UserId = cmd.Result.Id
 	}
 
-	// initialize session
-	if err := ctx.Session.Start(ctx.Context); err != nil {
-		log.Error(3, "Failed to start session", err)
-		return false
+	if err := bus.Dispatch(query); err != nil {
+		ctx.Handle(500, "Failed to find user", err)
+		return true
 	}
 
 	// Make sure that we cannot share a session between different users!
@@ -77,16 +144,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 		}
 	}
 
-	// When ldap is enabled, sync userinfo and org roles
-	if err := syncGrafanaUserWithLdapUser(ctx, query); err != nil {
-		if err == login.ErrInvalidCredentials {
-			ctx.Handle(500, "Unable to authenticate user", err)
-			return false
-		}
-
-		ctx.Handle(500, "Failed to sync user", err)
-		return false
-	}
+	ctx.Session.Set(AUTH_PROXY_SESSION_VAR, proxyHeaderValue)
 
 	ctx.SignedInUser = query.Result
 	ctx.IsSignedIn = true
@@ -95,29 +153,29 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 	return true
 }
 
-var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
-	if !setting.LdapEnabled {
-		return nil
-	}
-
+var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
 	expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
 
 	var lastLdapSync int64
-	if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
+	if lastLdapSyncInSession := query.ReqContext.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
 		lastLdapSync = lastLdapSyncInSession.(int64)
 	}
 
 	if lastLdapSync < expireEpoch {
 		ldapCfg := login.LdapCfg
 
+		if len(ldapCfg.Servers) < 1 {
+			return fmt.Errorf("No LDAP servers available")
+		}
+
 		for _, server := range ldapCfg.Servers {
 			author := login.NewLdapAuthenticator(server)
-			if err := author.SyncSignedInUser(query.Result); err != nil {
+			if err := author.SyncUser(query); err != nil {
 				return err
 			}
 		}
 
-		ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
+		query.ReqContext.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
 	}
 
 	return nil
@@ -143,29 +201,3 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
 
 	return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
 }
-
-func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
-	query := m.GetSignedInUserQuery{}
-	if setting.AuthProxyHeaderProperty == "username" {
-		query.Login = headerVal
-	} else if setting.AuthProxyHeaderProperty == "email" {
-		query.Email = headerVal
-	} else {
-		panic("Auth proxy header property invalid")
-	}
-	return &query
-}
-
-func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand {
-	cmd := m.CreateUserCommand{}
-	if setting.AuthProxyHeaderProperty == "username" {
-		cmd.Login = headerVal
-		cmd.Email = headerVal
-	} else if setting.AuthProxyHeaderProperty == "email" {
-		cmd.Email = headerVal
-		cmd.Login = headerVal
-	} else {
-		panic("Auth proxy header property invalid")
-	}
-	return &cmd
-}

+ 36 - 24
pkg/middleware/auth_proxy_test.go

@@ -26,57 +26,71 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) {
 			return &mockLdapAuther
 		}
 
-		signedInUser := m.SignedInUser{}
-		query := m.GetSignedInUserQuery{Result: &signedInUser}
-
-		Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() {
+		Convey("When user logs in, call SyncUser", func() {
 			// arrange
-			sess := mockSession{}
+			sess := newMockSession()
 			ctx := m.ReqContext{Session: &sess}
 			So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
 
 			// act
-			syncGrafanaUserWithLdapUser(&ctx, &query)
+			syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
+				ReqContext: &ctx,
+				Username:   "test",
+			})
 
 			// assert
-			So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
+			So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
 			So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0)
 		})
 
 		Convey("When session variable not expired, don't sync and don't change session var", func() {
 			// arrange
-			sess := mockSession{}
+			sess := newMockSession()
 			ctx := m.ReqContext{Session: &sess}
 			now := time.Now().Unix()
 			sess.Set(session.SESS_KEY_LASTLDAPSYNC, now)
+			sess.Set(AUTH_PROXY_SESSION_VAR, "test")
 
 			// act
-			syncGrafanaUserWithLdapUser(&ctx, &query)
+			syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
+				ReqContext: &ctx,
+				Username:   "test",
+			})
 
 			// assert
 			So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
-			So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse)
+			So(mockLdapAuther.syncUserCalled, ShouldBeFalse)
 		})
 
 		Convey("When lastldapsync is expired, session variable should be updated", func() {
 			// arrange
-			sess := mockSession{}
+			sess := newMockSession()
 			ctx := m.ReqContext{Session: &sess}
 			expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
 			sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime)
+			sess.Set(AUTH_PROXY_SESSION_VAR, "test")
 
 			// act
-			syncGrafanaUserWithLdapUser(&ctx, &query)
+			syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
+				ReqContext: &ctx,
+				Username:   "test",
+			})
 
 			// assert
 			So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
-			So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
+			So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
 		})
 	})
 }
 
 type mockSession struct {
-	value interface{}
+	value map[interface{}]interface{}
+}
+
+func newMockSession() mockSession {
+	session := mockSession{}
+	session.value = make(map[interface{}]interface{})
+	return session
 }
 
 func (s *mockSession) Start(c *macaron.Context) error {
@@ -84,15 +98,16 @@ func (s *mockSession) Start(c *macaron.Context) error {
 }
 
 func (s *mockSession) Set(k interface{}, v interface{}) error {
-	s.value = v
+	s.value[k] = v
 	return nil
 }
 
 func (s *mockSession) Get(k interface{}) interface{} {
-	return s.value
+	return s.value[k]
 }
 
 func (s *mockSession) Delete(k interface{}) interface{} {
+	delete(s.value, k)
 	return nil
 }
 
@@ -113,21 +128,18 @@ func (s *mockSession) RegenerateId(c *macaron.Context) error {
 }
 
 type mockLdapAuthenticator struct {
-	syncSignedInUserCalled bool
+	syncUserCalled bool
 }
 
-func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error {
+func (a *mockLdapAuthenticator) Login(query *m.LoginUserQuery) error {
 	return nil
 }
 
-func (a *mockLdapAuthenticator) SyncSignedInUser(signedInUser *m.SignedInUser) error {
-	a.syncSignedInUserCalled = true
+func (a *mockLdapAuthenticator) SyncUser(query *m.LoginUserQuery) error {
+	a.syncUserCalled = true
 	return nil
 }
 
-func (a *mockLdapAuthenticator) GetGrafanaUserFor(ldapUser *login.LdapUserInfo) (*m.User, error) {
+func (a *mockLdapAuthenticator) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *login.LdapUserInfo) (*m.User, error) {
 	return nil, nil
 }
-func (a *mockLdapAuthenticator) SyncOrgRoles(user *m.User, ldapUser *login.LdapUserInfo) error {
-	return nil
-}

+ 1 - 2
pkg/middleware/middleware.go

@@ -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

+ 27 - 5
pkg/middleware/middleware_test.go

@@ -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
 			})
 
@@ -177,12 +176,18 @@ func TestMiddlewareContext(t *testing.T) {
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
 			setting.AuthProxyHeaderProperty = "username"
+			setting.LdapEnabled = false
 
 			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
 				query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
 				return nil
 			})
 
+			bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+				cmd.Result = &m.User{Id: 12}
+				return nil
+			})
+
 			sc.fakeReq("GET", "/")
 			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
 			sc.exec()
@@ -199,6 +204,7 @@ func TestMiddlewareContext(t *testing.T) {
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
 			setting.AuthProxyHeaderProperty = "username"
 			setting.AuthProxyAutoSignUp = true
+			setting.LdapEnabled = false
 
 			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
 				if query.UserId > 0 {
@@ -209,8 +215,8 @@ func TestMiddlewareContext(t *testing.T) {
 				}
 			})
 
-			bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
-				cmd.Result = m.User{Id: 33}
+			bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+				cmd.Result = &m.User{Id: 33}
 				return nil
 			})
 
@@ -271,6 +277,11 @@ func TestMiddlewareContext(t *testing.T) {
 				return nil
 			})
 
+			bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+				cmd.Result = &m.User{Id: 33}
+				return nil
+			})
+
 			sc.fakeReq("GET", "/")
 			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
 			sc.req.RemoteAddr = "[2001::23]:12345"
@@ -289,6 +300,11 @@ func TestMiddlewareContext(t *testing.T) {
 			setting.AuthProxyHeaderProperty = "username"
 			setting.AuthProxyWhitelist = ""
 
+			bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
+				query.Result = &m.User{Id: 32}
+				return nil
+			})
+
 			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
 				query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
 				return nil
@@ -319,11 +335,17 @@ func TestMiddlewareContext(t *testing.T) {
 			setting.LdapEnabled = true
 
 			called := false
-			syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
+			syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
 				called = true
+				query.User = &m.User{Id: 32}
 				return nil
 			}
 
+			bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
+				query.Result = &m.User{Id: 32}
+				return nil
+			})
+
 			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
 				query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
 				return nil

+ 2 - 2
pkg/models/org_user.go

@@ -48,9 +48,9 @@ func (r *RoleType) UnmarshalJSON(data []byte) error {
 
 	*r = RoleType(str)
 
-	if (*r).IsValid() == false {
+	if !(*r).IsValid() {
 		if (*r) != "" {
-			return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r))
+			return fmt.Errorf("JSON validation error: invalid role value: %s", *r)
 		}
 
 		*r = ROLE_VIEWER

+ 72 - 0
pkg/models/user_auth.go

@@ -0,0 +1,72 @@
+package models
+
+import (
+	"time"
+)
+
+type UserAuth struct {
+	Id         int64
+	UserId     int64
+	AuthModule string
+	AuthId     string
+	Created    time.Time
+}
+
+type ExternalUserInfo struct {
+	AuthModule string
+	AuthId     string
+	UserId     int64
+	Email      string
+	Login      string
+	Name       string
+	OrgRoles   map[int64]RoleType
+}
+
+// ---------------------
+// COMMANDS
+
+type UpsertUserCommand struct {
+	ReqContext    *ReqContext
+	ExternalUser  *ExternalUserInfo
+	SignupAllowed bool
+
+	Result *User
+}
+
+type SetAuthInfoCommand struct {
+	AuthModule string
+	AuthId     string
+	UserId     int64
+}
+
+type DeleteAuthInfoCommand struct {
+	UserAuth *UserAuth
+}
+
+// ----------------------
+// QUERIES
+
+type LoginUserQuery struct {
+	ReqContext *ReqContext
+	Username   string
+	Password   string
+	User       *User
+	IpAddress  string
+}
+
+type GetUserByAuthInfoQuery struct {
+	AuthModule string
+	AuthId     string
+	UserId     int64
+	Email      string
+	Login      string
+
+	Result *User
+}
+
+type GetAuthInfoQuery struct {
+	AuthModule string
+	AuthId     string
+
+	Result *UserAuth
+}

+ 1 - 1
pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go

@@ -74,7 +74,7 @@ func TestMappingRowValue(t *testing.T) {
 
 	boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
 	haveBool, ok := boolRowValue.(bool)
-	if !ok || haveBool != true {
+	if !ok || !haveBool {
 		t.Fatalf("Expected true, was %v", haveBool)
 	}
 

+ 3 - 3
pkg/services/alerting/conditions/evaluator.go

@@ -20,7 +20,7 @@ type AlertEvaluator interface {
 type NoValueEvaluator struct{}
 
 func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
-	return reducedValue.Valid == false
+	return !reducedValue.Valid
 }
 
 type ThresholdEvaluator struct {
@@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
 }
 
 func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
-	if reducedValue.Valid == false {
+	if !reducedValue.Valid {
 		return false
 	}
 
@@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
 }
 
 func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
-	if reducedValue.Valid == false {
+	if !reducedValue.Valid {
 		return false
 	}
 

+ 1 - 1
pkg/services/alerting/conditions/query.go

@@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
 		reducedValue := c.Reducer.Reduce(series)
 		evalMatch := c.Evaluator.Eval(reducedValue)
 
-		if reducedValue.Valid == false {
+		if !reducedValue.Valid {
 			emptySerieCount++
 		}
 

+ 1 - 1
pkg/services/alerting/extractor.go

@@ -104,7 +104,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
 
 		// backward compatibility check, can be removed later
 		enabled, hasEnabled := jsonAlert.CheckGet("enabled")
-		if hasEnabled && enabled.MustBool() == false {
+		if hasEnabled && !enabled.MustBool() {
 			continue
 		}
 

+ 1 - 1
pkg/services/alerting/notifiers/telegram.go

@@ -219,7 +219,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
 
 func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
 	var cmd *m.SendWebhookSync
-	if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
+	if evalContext.ImagePublicUrl == "" && this.UploadImage {
 		cmd = this.buildMessage(evalContext, true)
 	} else {
 		cmd = this.buildMessage(evalContext, false)

+ 2 - 2
pkg/services/alerting/rule.go

@@ -55,8 +55,8 @@ func (e ValidationError) Error() string {
 }
 
 var (
-	ValueFormatRegex = regexp.MustCompile("^\\d+")
-	UnitFormatRegex  = regexp.MustCompile("\\w{1}$")
+	ValueFormatRegex = regexp.MustCompile(`^\d+`)
+	UnitFormatRegex  = regexp.MustCompile(`\w{1}$`)
 )
 
 var unitMultiplier = map[string]int{

+ 2 - 2
pkg/services/alerting/scheduler.go

@@ -15,7 +15,7 @@ type SchedulerImpl struct {
 
 func NewScheduler() Scheduler {
 	return &SchedulerImpl{
-		jobs: make(map[int64]*Job, 0),
+		jobs: make(map[int64]*Job),
 		log:  log.New("alerting.scheduler"),
 	}
 }
@@ -23,7 +23,7 @@ func NewScheduler() Scheduler {
 func (s *SchedulerImpl) Update(rules []*Rule) {
 	s.log.Debug("Scheduling update", "ruleCount", len(rules))
 
-	jobs := make(map[int64]*Job, 0)
+	jobs := make(map[int64]*Job)
 
 	for i, rule := range rules {
 		var job *Job

+ 1 - 2
pkg/services/notifications/mailer.go

@@ -7,7 +7,6 @@ package notifications
 import (
 	"bytes"
 	"crypto/tls"
-	"errors"
 	"fmt"
 	"html/template"
 	"net"
@@ -135,7 +134,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
 		subjectText, hasSubject := subjectData["value"]
 
 		if !hasSubject {
-			return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
+			return nil, fmt.Errorf("Missing subject in Template %s", cmd.Template)
 		}
 
 		subjectTmpl, err := template.New("subject").Parse(subjectText.(string))

+ 1 - 5
pkg/services/provisioning/provisioning.go

@@ -20,11 +20,7 @@ func Init(ctx context.Context, homePath string, cfg *ini.File) error {
 
 	dashboardPath := path.Join(provisioningPath, "dashboards")
 	_, err := dashboards.Provision(ctx, dashboardPath)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 func makeAbsolute(path string, root string) string {

+ 1 - 6
pkg/services/sqlstore/alert_notification.go

@@ -23,12 +23,7 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
 		_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
-
-		if err != nil {
-			return err
-		}
-
-		return nil
+		return err
 	})
 }
 

+ 2 - 5
pkg/services/sqlstore/annotation.go

@@ -102,11 +102,8 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
 
 		existing.Tags = item.Tags
 
-		if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
-			return err
-		}
-
-		return nil
+		_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing)
+		return err
 	})
 }
 

+ 2 - 2
pkg/services/sqlstore/apikey.go

@@ -55,7 +55,7 @@ func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrInvalidApiKey
 	}
 
@@ -69,7 +69,7 @@ func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrInvalidApiKey
 	}
 

+ 8 - 18
pkg/services/sqlstore/dashboard.go

@@ -63,7 +63,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
 		}
 
 		// do not allow plugin dashboard updates without overwrite flag
-		if existing.PluginId != "" && cmd.Overwrite == false {
+		if existing.PluginId != "" && !cmd.Overwrite {
 			return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
 		}
 	}
@@ -172,7 +172,7 @@ func GetDashboard(query *m.GetDashboardQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrDashboardNotFound
 	}
 
@@ -308,7 +308,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 		has, err := sess.Get(&dashboard)
 		if err != nil {
 			return err
-		} else if has == false {
+		} else if !has {
 			return m.ErrDashboardNotFound
 		}
 
@@ -347,12 +347,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
 
 	err := x.In("id", query.DashboardIds).Find(&dashboards)
 	query.Result = dashboards
-
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 // GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
@@ -431,12 +426,7 @@ func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
 
 	err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
 	query.Result = dashboards
-
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 type DashboardSlugDTO struct {
@@ -451,7 +441,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if exists == false {
+	} else if !exists {
 		return m.ErrDashboardNotFound
 	}
 
@@ -479,7 +469,7 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if exists == false {
+	} else if !exists {
 		return m.ErrDashboardNotFound
 	}
 
@@ -569,7 +559,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 	}
 
 	// do not allow plugin dashboard updates without overwrite flag
-	if existing.PluginId != "" && cmd.Overwrite == false {
+	if existing.PluginId != "" && !cmd.Overwrite {
 		return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
 	}
 

+ 2 - 4
pkg/services/sqlstore/dashboard_acl.go

@@ -35,10 +35,8 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
 
 		// Update dashboard HasAcl flag
 		dashboard := m.Dashboard{HasAcl: true}
-		if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
-			return err
-		}
-		return nil
+		_, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard)
+		return err
 	})
 }
 

+ 1 - 1
pkg/services/sqlstore/dashboard_snapshot.go

@@ -80,7 +80,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrDashboardSnapshotNotFound
 	}
 

+ 1 - 0
pkg/services/sqlstore/migrations/migrations.go

@@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) {
 	addDashboardAclMigrations(mg)
 	addTagMigration(mg)
 	addLoginAttemptMigrations(mg)
+	addUserAuthMigrations(mg)
 }
 
 func addMigrationLogMigrations(mg *Migrator) {

+ 24 - 0
pkg/services/sqlstore/migrations/user_auth_mig.go

@@ -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: 190, 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)
+}

+ 2 - 4
pkg/services/sqlstore/migrator/dialect.go

@@ -84,8 +84,7 @@ func (db *BaseDialect) DateTimeFunc(value string) string {
 }
 
 func (b *BaseDialect) CreateTableSql(table *Table) string {
-	var sql string
-	sql = "CREATE TABLE IF NOT EXISTS "
+	sql := "CREATE TABLE IF NOT EXISTS "
 	sql += b.dialect.Quote(table.Name) + " (\n"
 
 	pkList := table.PrimaryKeys
@@ -162,8 +161,7 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
 
 func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
 	quote := db.dialect.Quote
-	var name string
-	name = index.XName(tableName)
+	name := index.XName(tableName)
 	return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
 }
 

+ 1 - 2
pkg/services/sqlstore/migrator/migrations.go

@@ -1,7 +1,6 @@
 package migrator
 
 import (
-	"fmt"
 	"strings"
 )
 
@@ -113,7 +112,7 @@ func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
 
 func (m *DropIndexMigration) Sql(dialect Dialect) string {
 	if m.index.Name == "" {
-		m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_"))
+		m.index.Name = strings.Join(m.index.Cols, "_")
 	}
 	return dialect.DropIndexSql(m.tableName, m.index)
 }

+ 1 - 1
pkg/services/sqlstore/migrator/types.go

@@ -46,7 +46,7 @@ type Index struct {
 
 func (index *Index) XName(tableName string) string {
 	if index.Name == "" {
-		index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_"))
+		index.Name = strings.Join(index.Cols, "_")
 	}
 
 	if !strings.HasPrefix(index.Name, "UQE_") &&

+ 1 - 1
pkg/services/sqlstore/plugin_setting.go

@@ -36,7 +36,7 @@ func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
 	has, err := x.Get(&pluginSetting)
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrPluginSettingNotFound
 	}
 	query.Result = &pluginSetting

+ 4 - 4
pkg/services/sqlstore/quota.go

@@ -31,7 +31,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
 	has, err := x.Get(&quota)
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		quota.Limit = query.Default
 	}
 
@@ -108,7 +108,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
 			return err
 		}
 		quota.Limit = cmd.Limit
-		if has == false {
+		if !has {
 			quota.Created = time.Now()
 			//No quota in the DB for this target, so create a new one.
 			if _, err := sess.Insert(&quota); err != nil {
@@ -133,7 +133,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
 	has, err := x.Get(&quota)
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		quota.Limit = query.Default
 	}
 
@@ -210,7 +210,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
 			return err
 		}
 		quota.Limit = cmd.Limit
-		if has == false {
+		if !has {
 			quota.Created = time.Now()
 			//No quota in the DB for this target, so create a new one.
 			if _, err := sess.Insert(&quota); err != nil {

+ 0 - 4
pkg/services/sqlstore/stats.go

@@ -19,10 +19,6 @@ func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
 	var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
 	query.Result = make([]*m.DataSourceStats, 0)
 	err := x.SQL(rawSql).Find(&query.Result)
-	if err != nil {
-		return err
-	}
-
 	return err
 }
 

+ 1 - 5
pkg/services/sqlstore/team.go

@@ -210,11 +210,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 	sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
 
 	err := sess.Find(&query.Result)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return err
 }
 
 // AddTeamMember adds a user to a team

+ 1 - 1
pkg/services/sqlstore/temp_user.go

@@ -126,7 +126,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrTempUserNotFound
 	}
 

+ 17 - 24
pkg/services/sqlstore/user.go

@@ -154,7 +154,7 @@ func GetUserById(query *m.GetUserByIdQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -179,7 +179,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
 		return err
 	}
 
-	if has == false && strings.Contains(query.LoginOrEmail, "@") {
+	if !has && strings.Contains(query.LoginOrEmail, "@") {
 		// If the user wasn't found, and it contains an "@" fallback to finding the
 		// user by email.
 		user = &m.User{Email: query.LoginOrEmail}
@@ -188,7 +188,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -209,7 +209,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -253,11 +253,8 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
 			Updated:  time.Now(),
 		}
 
-		if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
-			return err
-		}
-
-		return nil
+		_, err := sess.Id(cmd.UserId).Update(&user)
+		return err
 	})
 }
 
@@ -271,11 +268,8 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
 			LastSeenAt: time.Now(),
 		}
 
-		if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
-			return err
-		}
-
-		return nil
+		_, err := sess.Id(cmd.UserId).Update(&user)
+		return err
 	})
 }
 
@@ -295,11 +289,12 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
 	}
 
 	return inTransaction(func(sess *DBSession) error {
-		user := m.User{}
-		sess.Id(cmd.UserId).Get(&user)
+		user := m.User{
+			Id:    cmd.UserId,
+			OrgId: cmd.OrgId,
+		}
 
-		user.OrgId = cmd.OrgId
-		_, err := sess.Id(user.Id).Update(&user)
+		_, err := sess.Id(cmd.UserId).Update(&user)
 		return err
 	})
 }
@@ -310,7 +305,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
 
 	if err != nil {
 		return err
-	} else if has == false {
+	} else if !has {
 		return m.ErrUserNotFound
 	}
 
@@ -445,6 +440,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 {
@@ -479,10 +475,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
 			Updated:    time.Now(),
 		}
 
-		if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
-			return err
-		}
-
-		return nil
+		_, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
+		return err
 	})
 }

+ 148 - 0
pkg/services/sqlstore/user_auth.go

@@ -0,0 +1,148 @@
+package sqlstore
+
+import (
+	"time"
+
+	"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 := &m.User{}
+	has := false
+	var err error
+	authQuery := &m.GetAuthInfoQuery{}
+
+	// Try to find the user by auth module and id first
+	if query.AuthModule != "" && query.AuthId != "" {
+		authQuery.AuthModule = query.AuthModule
+		authQuery.AuthId = query.AuthId
+
+		err = GetAuthInfo(authQuery)
+		if err != m.ErrUserNotFound {
+			if err != nil {
+				return err
+			}
+
+			// if user id was specified and doesn't match the user_auth entry, remove it
+			if query.UserId != 0 && query.UserId != authQuery.Result.UserId {
+				err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+					UserAuth: authQuery.Result,
+				})
+				if err != nil {
+					sqlog.Error("Error removing user_auth entry", "error", err)
+				}
+
+				authQuery.Result = nil
+			} else {
+				has, err = x.Id(authQuery.Result.UserId).Get(user)
+				if err != nil {
+					return err
+				}
+
+				if !has {
+					// if the user has been deleted then remove the entry
+					err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+						UserAuth: authQuery.Result,
+					})
+					if err != nil {
+						sqlog.Error("Error removing user_auth entry", "error", err)
+					}
+
+					authQuery.Result = nil
+				}
+			}
+		}
+	}
+
+	// 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
+	}
+
+	// create authInfo record to link accounts
+	if authQuery.Result == nil && query.AuthModule != "" && query.AuthId != "" {
+		cmd2 := &m.SetAuthInfoCommand{
+			UserId:     user.Id,
+			AuthModule: query.AuthModule,
+			AuthId:     query.AuthId,
+		}
+		if err := SetAuthInfo(cmd2); err != nil {
+			return err
+		}
+	}
+
+	query.Result = 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.Result = 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,
+			Created:    time.Now(),
+		}
+
+		_, err := sess.Insert(authUser)
+		return err
+	})
+}
+
+func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error {
+	return inTransaction(func(sess *DBSession) error {
+		_, err := sess.Delete(cmd.UserAuth)
+		return err
+	})
+}

+ 131 - 0
pkg/services/sqlstore/user_auth_test.go

@@ -0,0 +1,131 @@
+package sqlstore
+
+import (
+	"fmt"
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func TestUserAuth(t *testing.T) {
+	InitTestDB(t)
+
+	Convey("Given 5 users", t, func() {
+		var err error
+		var cmd *m.CreateUserCommand
+		users := []m.User{}
+		for i := 0; i < 5; i++ {
+			cmd = &m.CreateUserCommand{
+				Email: fmt.Sprint("user", i, "@test.com"),
+				Name:  fmt.Sprint("user", i),
+				Login: fmt.Sprint("loginuser", i),
+			}
+			err = CreateUser(cmd)
+			So(err, ShouldBeNil)
+			users = append(users, cmd.Result)
+		}
+
+		Reset(func() {
+			_, err := x.Exec("DELETE FROM org_user WHERE 1=1")
+			So(err, ShouldBeNil)
+			_, err = x.Exec("DELETE FROM org WHERE 1=1")
+			So(err, ShouldBeNil)
+			_, err = x.Exec("DELETE FROM user WHERE 1=1")
+			So(err, ShouldBeNil)
+			_, err = x.Exec("DELETE FROM user_auth WHERE 1=1")
+			So(err, ShouldBeNil)
+		})
+
+		Convey("Can find existing user", func() {
+			// By Login
+			login := "loginuser0"
+
+			query := &m.GetUserByAuthInfoQuery{Login: login}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldBeNil)
+			So(query.Result.Login, ShouldEqual, login)
+
+			// By ID
+			id := query.Result.Id
+
+			query = &m.GetUserByAuthInfoQuery{UserId: id}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldBeNil)
+			So(query.Result.Id, ShouldEqual, id)
+
+			// By Email
+			email := "user1@test.com"
+
+			query = &m.GetUserByAuthInfoQuery{Email: email}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldBeNil)
+			So(query.Result.Email, ShouldEqual, email)
+
+			// Don't find nonexistent user
+			email = "nonexistent@test.com"
+
+			query = &m.GetUserByAuthInfoQuery{Email: email}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldEqual, m.ErrUserNotFound)
+			So(query.Result, ShouldBeNil)
+		})
+
+		Convey("Can set & locate by AuthModule and AuthId", func() {
+			// get nonexistent user_auth entry
+			query := &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldEqual, m.ErrUserNotFound)
+			So(query.Result, ShouldBeNil)
+
+			// create user_auth entry
+			login := "loginuser0"
+
+			query.Login = login
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldBeNil)
+			So(query.Result.Login, ShouldEqual, login)
+
+			// get via user_auth
+			query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldBeNil)
+			So(query.Result.Login, ShouldEqual, login)
+
+			// get with non-matching id
+			id := query.Result.Id
+
+			query.UserId = id + 1
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldBeNil)
+			So(query.Result.Login, ShouldEqual, "loginuser1")
+
+			// get via user_auth
+			query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldBeNil)
+			So(query.Result.Login, ShouldEqual, "loginuser1")
+
+			// remove user
+			_, err = x.Exec("DELETE FROM user WHERE id=?", query.Result.Id)
+			So(err, ShouldBeNil)
+
+			// get via user_auth for deleted user
+			query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+			err = GetUserByAuthInfo(query)
+
+			So(err, ShouldEqual, m.ErrUserNotFound)
+			So(query.Result, ShouldBeNil)
+		})
+	})
+}

+ 1 - 1
pkg/social/generic_oauth.go

@@ -182,7 +182,7 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
 	var data UserInfoJson
 	var err error
 
-	if s.extractToken(&data, token) != true {
+	if !s.extractToken(&data, token) {
 		response, err := HttpGet(client, s.apiUrl)
 		if err != nil {
 			return nil, fmt.Errorf("Error getting user info: %s", err)

+ 2 - 0
pkg/social/grafana_com_oauth.go

@@ -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,

+ 1 - 0
pkg/social/social.go

@@ -14,6 +14,7 @@ import (
 )
 
 type BasicUserInfo struct {
+	Id      string
 	Name    string
 	Email   string
 	Login   string

+ 0 - 3
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -71,15 +71,12 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
 	switch queryType {
 	case "metricFindQuery":
 		result, err = e.executeMetricFindQuery(ctx, queryContext)
-		break
 	case "annotationQuery":
 		result, err = e.executeAnnotationQuery(ctx, queryContext)
-		break
 	case "timeSeriesQuery":
 		fallthrough
 	default:
 		result, err = e.executeTimeSeriesQuery(ctx, queryContext)
-		break
 	}
 
 	return result, err

+ 4 - 15
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -175,25 +175,18 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
 	switch subType {
 	case "regions":
 		data, err = e.handleGetRegions(ctx, parameters, queryContext)
-		break
 	case "namespaces":
 		data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
-		break
 	case "metrics":
 		data, err = e.handleGetMetrics(ctx, parameters, queryContext)
-		break
 	case "dimension_keys":
 		data, err = e.handleGetDimensions(ctx, parameters, queryContext)
-		break
 	case "dimension_values":
 		data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
-		break
 	case "ebs_volume_ids":
 		data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
-		break
 	case "ec2_instance_attribute":
 		data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
-		break
 	}
 
 	transformToTable(data, queryResult)
@@ -261,7 +254,7 @@ func (e *CloudWatchExecutor) handleGetNamespaces(ctx context.Context, parameters
 		keys = append(keys, strings.Split(customNamespaces, ",")...)
 	}
 
-	sort.Sort(sort.StringSlice(keys))
+	sort.Strings(keys)
 
 	result := make([]suggestData, 0)
 	for _, key := range keys {
@@ -290,7 +283,7 @@ func (e *CloudWatchExecutor) handleGetMetrics(ctx context.Context, parameters *s
 			return nil, errors.New("Unable to call AWS API")
 		}
 	}
-	sort.Sort(sort.StringSlice(namespaceMetrics))
+	sort.Strings(namespaceMetrics)
 
 	result := make([]suggestData, 0)
 	for _, name := range namespaceMetrics {
@@ -319,7 +312,7 @@ func (e *CloudWatchExecutor) handleGetDimensions(ctx context.Context, parameters
 			return nil, errors.New("Unable to call AWS API")
 		}
 	}
-	sort.Sort(sort.StringSlice(dimensionValues))
+	sort.Strings(dimensionValues)
 
 	result := make([]suggestData, 0)
 	for _, name := range dimensionValues {
@@ -573,11 +566,7 @@ func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error)
 			}
 			return !lastPage
 		})
-	if err != nil {
-		return resp, err
-	}
-
-	return resp, nil
+	return resp, err
 }
 
 var metricsCacheLock sync.Mutex

+ 1 - 4
pkg/tsdb/cloudwatch/metric_find_query_test.go

@@ -181,10 +181,7 @@ func TestCloudWatchMetrics(t *testing.T) {
 }
 
 func TestParseMultiSelectValue(t *testing.T) {
-
-	var values []string
-
-	values = parseMultiSelectValue(" i-someInstance ")
+	values := parseMultiSelectValue(" i-someInstance ")
 	assert.Equal(t, []string{"i-someInstance"}, values)
 
 	values = parseMultiSelectValue("{i-05}")

+ 5 - 5
pkg/tsdb/mssql/mssql.go

@@ -145,7 +145,7 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
 	// convert types not handled by denisenkom/go-mssqldb
 	// unhandled types are returned as []byte
 	for i := 0; i < len(types); i++ {
-		if value, ok := values[i].([]byte); ok == true {
+		if value, ok := values[i].([]byte); ok {
 			switch types[i].DatabaseTypeName() {
 			case "MONEY", "SMALLMONEY", "DECIMAL":
 				if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@@ -209,7 +209,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 	fillValue := null.Float{}
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if query.Model.Get("fillNull").MustBool(false) == false {
+		if !query.Model.Get("fillNull").MustBool(false) {
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 		}
@@ -244,7 +244,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 		}
 
 		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok == true {
+			if columnValue, ok := values[metricIndex].(string); ok {
 				metric = columnValue
 			} else {
 				return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
@@ -271,7 +271,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 			}
 
 			series, exist := pointsBySeries[metric]
-			if exist == false {
+			if !exist {
 				series = &tsdb.TimeSeries{Name: metric}
 				pointsBySeries[metric] = series
 				seriesByQueryOrder.PushBack(metric)
@@ -279,7 +279,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 
 			if fillMissing {
 				var intervalStart float64
-				if exist == false {
+				if !exist {
 					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
 				} else {
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

+ 4 - 4
pkg/tsdb/mysql/mysql.go

@@ -218,7 +218,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 	fillValue := null.Float{}
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if query.Model.Get("fillNull").MustBool(false) == false {
+		if !query.Model.Get("fillNull").MustBool(false) {
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 		}
@@ -253,7 +253,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 		}
 
 		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok == true {
+			if columnValue, ok := values[metricIndex].(string); ok {
 				metric = columnValue
 			} else {
 				return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@@ -280,7 +280,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 			}
 
 			series, exist := pointsBySeries[metric]
-			if exist == false {
+			if !exist {
 				series = &tsdb.TimeSeries{Name: metric}
 				pointsBySeries[metric] = series
 				seriesByQueryOrder.PushBack(metric)
@@ -288,7 +288,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 
 			if fillMissing {
 				var intervalStart float64
-				if exist == false {
+				if !exist {
 					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
 				} else {
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

+ 5 - 5
pkg/tsdb/postgres/postgres.go

@@ -131,7 +131,7 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
 	// convert types not handled by lib/pq
 	// unhandled types are returned as []byte
 	for i := 0; i < len(types); i++ {
-		if value, ok := values[i].([]byte); ok == true {
+		if value, ok := values[i].([]byte); ok {
 			switch types[i].DatabaseTypeName() {
 			case "NUMERIC":
 				if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@@ -198,7 +198,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 	fillValue := null.Float{}
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if query.Model.Get("fillNull").MustBool(false) == false {
+		if !query.Model.Get("fillNull").MustBool(false) {
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 		}
@@ -233,7 +233,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 		}
 
 		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok == true {
+			if columnValue, ok := values[metricIndex].(string); ok {
 				metric = columnValue
 			} else {
 				return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@@ -260,7 +260,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 			}
 
 			series, exist := pointsBySeries[metric]
-			if exist == false {
+			if !exist {
 				series = &tsdb.TimeSeries{Name: metric}
 				pointsBySeries[metric] = series
 				seriesByQueryOrder.PushBack(metric)
@@ -268,7 +268,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 
 			if fillMissing {
 				var intervalStart float64
-				if exist == false {
+				if !exist {
 					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
 				} else {
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

+ 1 - 1
pkg/tsdb/sql_engine.go

@@ -51,7 +51,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
 	defer engineCache.Unlock()
 
 	if engine, present := engineCache.cache[dsInfo.Id]; present {
-		if version, _ := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
+		if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
 			e.XormEngine = engine
 			return nil
 		}

+ 1 - 5
pkg/util/shortid_generator.go

@@ -17,11 +17,7 @@ func init() {
 
 // IsValidShortUid checks if short unique identifier contains valid characters
 func IsValidShortUid(uid string) bool {
-	if !validUidPattern(uid) {
-		return false
-	}
-
-	return true
+	return validUidPattern(uid)
 }
 
 // GenerateShortUid generates a short unique identifier.

+ 4 - 0
public/app/core/components/ScrollBar/ScrollBar.tsx

@@ -54,6 +54,10 @@ export default class ScrollBar extends React.Component<Props, any> {
     return false;
   }
 
+  update() {
+    this.scrollbar.update();
+  }
+
   handleRef = ref => {
     this.container = ref;
   };

+ 1 - 1
public/app/core/components/sidemenu/sidemenu.html

@@ -37,7 +37,7 @@
         <i class="fa fa-fw fa-sign-in"></i>
       </span>
     </a>
-    <a href="{{ctrl.loginUrl}}">
+    <a href="{{ctrl.loginUrl}}" target="_self">
       <ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
         <li class="side-menu-header">
           <span class="sidemenu-item-text">Sign In</span>

+ 134 - 9
public/app/features/dashboard/dashgrid/AddPanelPanel.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 import _ from 'lodash';
-
+import classNames from 'classnames';
 import config from 'app/core/config';
 import { PanelModel } from '../panel_model';
 import { PanelContainer } from './PanelContainer';
 import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
 import store from 'app/core/store';
 import { LS_PANEL_COPY_KEY } from 'app/core/constants';
+import Highlighter from 'react-highlight-words';
 
 export interface AddPanelPanelProps {
   panel: PanelModel;
@@ -16,21 +17,42 @@ export interface AddPanelPanelProps {
 export interface AddPanelPanelState {
   filter: string;
   panelPlugins: any[];
+  copiedPanelPlugins: any[];
+  tab: string;
 }
 
 export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelPanelState> {
+  private scrollbar: ScrollBar;
+
   constructor(props) {
     super(props);
     this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
     this.renderPanelItem = this.renderPanelItem.bind(this);
+    this.panelSizeChanged = this.panelSizeChanged.bind(this);
 
     this.state = {
-      panelPlugins: this.getPanelPlugins(),
+      panelPlugins: this.getPanelPlugins(''),
+      copiedPanelPlugins: this.getCopiedPanelPlugins(''),
       filter: '',
+      tab: 'Add',
     };
   }
 
-  getPanelPlugins() {
+  componentDidMount() {
+    this.props.panel.events.on('panel-size-changed', this.panelSizeChanged);
+  }
+
+  componentWillUnmount() {
+    this.props.panel.events.off('panel-size-changed', this.panelSizeChanged);
+  }
+
+  panelSizeChanged() {
+    setTimeout(() => {
+      this.scrollbar.update();
+    });
+  }
+
+  getPanelPlugins(filter) {
     let panels = _.chain(config.panels)
       .filter({ hideFromList: false })
       .map(item => item)
@@ -39,6 +61,19 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
     // add special row type
     panels.push({ id: 'row', name: 'Row', sort: 8, info: { logos: { small: 'public/img/icn-row.svg' } } });
 
+    panels = this.filterPanels(panels, filter);
+
+    // add sort by sort property
+    return _.sortBy(panels, 'sort');
+  }
+
+  getCopiedPanelPlugins(filter) {
+    let panels = _.chain(config.panels)
+      .filter({ hideFromList: false })
+      .map(item => item)
+      .value();
+    let copiedPanels = [];
+
     let copiedPanelJson = store.get(LS_PANEL_COPY_KEY);
     if (copiedPanelJson) {
       let copiedPanel = JSON.parse(copiedPanelJson);
@@ -48,12 +83,13 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
         pluginCopy.name = copiedPanel.title;
         pluginCopy.sort = -1;
         pluginCopy.defaults = copiedPanel;
-        panels.push(pluginCopy);
+        copiedPanels.push(pluginCopy);
       }
     }
 
-    // add sort by sort property
-    return _.sortBy(panels, 'sort');
+    copiedPanels = this.filterPanels(copiedPanels, filter);
+
+    return _.sortBy(copiedPanels, 'sort');
   }
 
   onAddPanel = panelPluginInfo => {
@@ -92,28 +128,117 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
     dashboard.removePanel(dashboard.panels[0]);
   }
 
+  renderText(text: string) {
+    let searchWords = this.state.filter.split('');
+    return <Highlighter highlightClassName="highlight-search-match" textToHighlight={text} searchWords={searchWords} />;
+  }
+
   renderPanelItem(panel, index) {
     return (
       <div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}>
         <img className="add-panel__item-img" src={panel.info.logos.small} />
-        <div className="add-panel__item-name">{panel.name}</div>
+        <div className="add-panel__item-name">{this.renderText(panel.name)}</div>
       </div>
     );
   }
 
+  noCopiedPanelPlugins() {
+    return <div className="add-panel__no-panels">No copied panels yet.</div>;
+  }
+
+  filterChange(evt) {
+    this.setState({
+      filter: evt.target.value,
+      panelPlugins: this.getPanelPlugins(evt.target.value),
+      copiedPanelPlugins: this.getCopiedPanelPlugins(evt.target.value),
+    });
+  }
+
+  filterPanels(panels, filter) {
+    let regex = new RegExp(filter, 'i');
+    return panels.filter(panel => {
+      return regex.test(panel.name);
+    });
+  }
+
+  openCopy() {
+    this.setState({
+      tab: 'Copy',
+      filter: '',
+      panelPlugins: this.getPanelPlugins(''),
+      copiedPanelPlugins: this.getCopiedPanelPlugins(''),
+    });
+  }
+
+  openAdd() {
+    this.setState({
+      tab: 'Add',
+      filter: '',
+      panelPlugins: this.getPanelPlugins(''),
+      copiedPanelPlugins: this.getCopiedPanelPlugins(''),
+    });
+  }
+
   render() {
+    let addClass = classNames({
+      'active active--panel': this.state.tab === 'Add',
+      '': this.state.tab === 'Copy',
+    });
+
+    let copyClass = classNames({
+      '': this.state.tab === 'Add',
+      'active active--panel': this.state.tab === 'Copy',
+    });
+
+    let panelTab;
+
+    if (this.state.tab === 'Add') {
+      panelTab = this.state.panelPlugins.map(this.renderPanelItem);
+    } else if (this.state.tab === 'Copy') {
+      if (this.state.copiedPanelPlugins.length > 0) {
+        panelTab = this.state.copiedPanelPlugins.map(this.renderPanelItem);
+      } else {
+        panelTab = this.noCopiedPanelPlugins();
+      }
+    }
+
     return (
       <div className="panel-container add-panel-container">
         <div className="add-panel">
           <div className="add-panel__header">
             <i className="gicon gicon-add-panel" />
             <span className="add-panel__title">New Panel</span>
-            <span className="add-panel__sub-title">Select a visualization</span>
+            <ul className="gf-tabs">
+              <li className="gf-tabs-item">
+                <div className={'gf-tabs-link pointer ' + addClass} onClick={this.openAdd.bind(this)}>
+                  Add
+                </div>
+              </li>
+              <li className="gf-tabs-item">
+                <div className={'gf-tabs-link pointer ' + copyClass} onClick={this.openCopy.bind(this)}>
+                  Paste
+                </div>
+              </li>
+            </ul>
             <button className="add-panel__close" onClick={this.handleCloseAddPanel}>
               <i className="fa fa-close" />
             </button>
           </div>
-          <ScrollBar className="add-panel__items">{this.state.panelPlugins.map(this.renderPanelItem)}</ScrollBar>
+          <ScrollBar ref={element => (this.scrollbar = element)} className="add-panel__items">
+            <div className="add-panel__searchbar">
+              <label className="gf-form gf-form--grow gf-form--has-input-icon">
+                <input
+                  type="text"
+                  className="gf-form-input max-width-20"
+                  placeholder="Panel Search Filter"
+                  value={this.state.filter}
+                  onChange={this.filterChange.bind(this)}
+                />
+                <i className="gf-form-input-icon fa fa-search" />
+              </label>
+            </div>
+            {panelTab}
+          </ScrollBar>
         </div>
       </div>
     );

+ 102 - 0
public/app/features/dashboard/specs/AddPanelPanel.jest.tsx

@@ -0,0 +1,102 @@
+import React from 'react';
+import { AddPanelPanel } from './../dashgrid/AddPanelPanel';
+import { PanelModel } from '../panel_model';
+import { shallow } from 'enzyme';
+import config from '../../../core/config';
+
+jest.mock('app/core/store', () => ({
+  get: key => {
+    return null;
+  },
+  delete: key => {
+    return null;
+  },
+}));
+
+describe('AddPanelPanel', () => {
+  let wrapper, dashboardMock, getPanelContainer, panel;
+
+  beforeEach(() => {
+    config.panels = [
+      {
+        id: 'singlestat',
+        hideFromList: false,
+        name: 'Singlestat',
+        sort: 2,
+        info: {
+          logos: {
+            small: '',
+          },
+        },
+      },
+      {
+        id: 'hidden',
+        hideFromList: true,
+        name: 'Hidden',
+        sort: 100,
+        info: {
+          logos: {
+            small: '',
+          },
+        },
+      },
+      {
+        id: 'graph',
+        hideFromList: false,
+        name: 'Graph',
+        sort: 1,
+        info: {
+          logos: {
+            small: '',
+          },
+        },
+      },
+      {
+        id: 'alexander_zabbix',
+        hideFromList: false,
+        name: 'Zabbix',
+        sort: 100,
+        info: {
+          logos: {
+            small: '',
+          },
+        },
+      },
+      {
+        id: 'piechart',
+        hideFromList: false,
+        name: 'Piechart',
+        sort: 100,
+        info: {
+          logos: {
+            small: '',
+          },
+        },
+      },
+    ];
+
+    dashboardMock = { toggleRow: jest.fn() };
+
+    getPanelContainer = jest.fn().mockReturnValue({
+      getDashboard: jest.fn().mockReturnValue(dashboardMock),
+      getPanelLoader: jest.fn(),
+    });
+
+    panel = new PanelModel({ collapsed: false });
+    wrapper = shallow(<AddPanelPanel panel={panel} getPanelContainer={getPanelContainer} />);
+  });
+
+  it('should fetch all panels sorted with core plugins first', () => {
+    //console.log(wrapper.debug());
+    //console.log(wrapper.find('.add-panel__item').get(0).props.title);
+    expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Singlestat');
+    expect(wrapper.find('.add-panel__item').get(4).props.title).toBe('Piechart');
+  });
+
+  it('should filter', () => {
+    wrapper.find('input').simulate('change', { target: { value: 'p' } });
+
+    expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Piechart');
+    expect(wrapper.find('.add-panel__item').get(0).props.title).toBe('Graph');
+  });
+});

+ 4 - 4
public/app/features/panel/panel_ctrl.ts

@@ -194,8 +194,8 @@ export class PanelCtrl {
       });
 
       menu.push({
-        text: 'Add to Panel List',
-        click: 'ctrl.addToPanelList()',
+        text: 'Copy',
+        click: 'ctrl.copyPanel()',
         role: 'Editor',
       });
     }
@@ -260,9 +260,9 @@ export class PanelCtrl {
     });
   }
 
-  addToPanelList() {
+  copyPanel() {
     store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel()));
-    appEvents.emit('alert-success', ['Panel temporarily added to panel list']);
+    appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']);
   }
 
   replacePanel(newPanel, oldPanel) {

+ 3 - 3
public/app/plugins/datasource/graphite/datasource.ts

@@ -320,7 +320,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       method: 'GET',
       url: '/tags/autoComplete/tags',
       params: {
-        expr: _.map(expressions, expression => templateSrv.replace(expression)),
+        expr: _.map(expressions, expression => templateSrv.replace((expression || '').trim())),
       },
       // for cancellations
       requestId: options.requestId,
@@ -355,8 +355,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       method: 'GET',
       url: '/tags/autoComplete/values',
       params: {
-        expr: _.map(expressions, expression => templateSrv.replace(expression)),
-        tag: templateSrv.replace(tag),
+        expr: _.map(expressions, expression => templateSrv.replace((expression || '').trim())),
+        tag: templateSrv.replace((tag || '').trim()),
       },
       // for cancellations
       requestId: options.requestId,

+ 95 - 0
public/app/plugins/datasource/graphite/specs/datasource_specs.ts

@@ -222,4 +222,99 @@ describe('graphiteDatasource', function() {
       expect(results.length).to.be(2);
     });
   });
+
+  describe('querying for template variables', () => {
+    let results;
+    let requestOptions;
+
+    beforeEach(() => {
+      ctx.backendSrv.datasourceRequest = function(options) {
+        requestOptions = options;
+        return ctx.$q.when({
+          data: [{ target: 'prod1.count', datapoints: [[10, 1], [12, 1]] }],
+        });
+      };
+    });
+
+    it('should generate tags query', () => {
+      ctx.ds.metricFindQuery('tags()').then(data => {
+        results = data;
+      });
+
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/tags/autoComplete/tags');
+      expect(requestOptions.params.expr).to.eql([]);
+      expect(results).not.to.be(null);
+    });
+
+    it('should generate tags query with a filter expression', () => {
+      ctx.ds.metricFindQuery('tags(server=backend_01)').then(data => {
+        results = data;
+      });
+
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/tags/autoComplete/tags');
+      expect(requestOptions.params.expr).to.eql(['server=backend_01']);
+      expect(results).not.to.be(null);
+    });
+
+    it('should generate tag query for an expression with whitespace after', () => {
+      ctx.ds.metricFindQuery('tags(server=backend_01 )').then(data => {
+        results = data;
+      });
+
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/tags/autoComplete/tags');
+      expect(requestOptions.params.expr).to.eql(['server=backend_01']);
+      expect(results).not.to.be(null);
+    });
+
+    it('should generate tag values query for one tag', () => {
+      ctx.ds.metricFindQuery('tag_values(server)').then(data => {
+        results = data;
+      });
+
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/tags/autoComplete/values');
+      expect(requestOptions.params.tag).to.be('server');
+      expect(requestOptions.params.expr).to.eql([]);
+      expect(results).not.to.be(null);
+    });
+
+    it('should generate tag values query for a tag and expression', () => {
+      ctx.ds.metricFindQuery('tag_values(server,server=~backend*)').then(data => {
+        results = data;
+      });
+
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/tags/autoComplete/values');
+      expect(requestOptions.params.tag).to.be('server');
+      expect(requestOptions.params.expr).to.eql(['server=~backend*']);
+      expect(results).not.to.be(null);
+    });
+
+    it('should generate tag values query for a tag with whitespace after', () => {
+      ctx.ds.metricFindQuery('tag_values(server )').then(data => {
+        results = data;
+      });
+
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/tags/autoComplete/values');
+      expect(requestOptions.params.tag).to.be('server');
+      expect(requestOptions.params.expr).to.eql([]);
+      expect(results).not.to.be(null);
+    });
+
+    it('should generate tag values query for a tag and expression with whitespace after', () => {
+      ctx.ds.metricFindQuery('tag_values(server , server=~backend* )').then(data => {
+        results = data;
+      });
+
+      ctx.$rootScope.$apply();
+      expect(requestOptions.url).to.be('/tags/autoComplete/values');
+      expect(requestOptions.params.tag).to.be('server');
+      expect(requestOptions.params.expr).to.eql(['server=~backend*']);
+      expect(results).not.to.be(null);
+    });
+  });
 });

+ 19 - 2
public/sass/components/_panel_add_panel.scss

@@ -11,9 +11,12 @@
 }
 
 .add-panel__header {
-  padding: 5px 15px;
+  padding: 0 15px;
   display: flex;
   align-items: center;
+  background: $page-header-bg;
+  box-shadow: $page-header-shadow;
+  border-bottom: 1px solid $page-header-border-color;
 
   .gicon {
     font-size: 30px;
@@ -31,7 +34,7 @@
 
 .add-panel__title {
   font-size: $font-size-md;
-  margin-right: $spacer/2;
+  margin-right: $spacer*2;
 }
 
 .add-panel__sub-title {
@@ -47,6 +50,7 @@
   flex-direction: row;
   flex-wrap: wrap;
   overflow: auto;
+  height: 100%;
   align-content: flex-start;
   justify-content: space-around;
   position: relative;
@@ -84,3 +88,16 @@
 .add-panel__item-icon {
   padding: 2px;
 }
+
+.add-panel__searchbar {
+  width: 100%;
+  margin-bottom: 10px;
+  margin-top: 7px;
+}
+
+.add-panel__no-panels {
+  color: $text-color-weak;
+  font-style: italic;
+  width: 100%;
+  padding: 3px 8px;
+}

+ 5 - 7
public/sass/components/_tabs.scss

@@ -44,18 +44,16 @@
 
     &::before {
       display: block;
-      content: " ";
+      content: ' ';
       position: absolute;
       left: 0;
       right: 0;
       height: 2px;
       top: 0;
-      background-image: linear-gradient(
-        to right,
-        #ffd500 0%,
-        #ff4400 99%,
-        #ff4400 100%
-      );
+      background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%);
     }
   }
+  &.active--panel {
+    background: $panel-bg !important;
+  }
 }

+ 1 - 1
public/sass/components/_tags.scss

@@ -4,7 +4,7 @@
   display: inline-block;
   padding: 2px 4px;
   font-size: $font-size-base * 0.846;
-  font-weight: bold;
+  font-weight: $font-weight-semi-bold;
   line-height: 14px; // ensure proper line-height if floated
   color: $white;
   vertical-align: baseline;