Merge branch 'master' into docs-5.1
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[run]
|
[run]
|
||||||
init_cmds = [
|
init_cmds = [
|
||||||
["go", "run", "build.go", "build-server"],
|
["go", "run", "build.go", "-dev", "build-server"],
|
||||||
["./bin/grafana-server", "cfg:app_mode=development"]
|
["./bin/grafana-server", "cfg:app_mode=development"]
|
||||||
]
|
]
|
||||||
watch_all = true
|
watch_all = true
|
||||||
@@ -12,6 +12,6 @@ watch_dirs = [
|
|||||||
watch_exts = [".go", ".ini", ".toml"]
|
watch_exts = [".go", ".ini", ".toml"]
|
||||||
build_delay = 1500
|
build_delay = 1500
|
||||||
cmds = [
|
cmds = [
|
||||||
["go", "run", "build.go", "build"],
|
["go", "run", "build.go", "-dev", "build"],
|
||||||
["./bin/grafana-server", "cfg:app_mode=development"]
|
["./bin/grafana-server", "cfg:app_mode=development"]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
jobs:
|
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:
|
test-frontend:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:6.11.4
|
- image: circleci/node:6.11.4
|
||||||
@@ -103,6 +119,10 @@ workflows:
|
|||||||
version: 2
|
version: 2
|
||||||
test-and-build:
|
test-and-build:
|
||||||
jobs:
|
jobs:
|
||||||
|
- codespell:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
- build:
|
- build:
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -54,9 +54,12 @@
|
|||||||
* **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm)
|
* **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)
|
* **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**: 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)
|
* **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
|
### 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
|
* Migrated JavaScript files to TypeScript
|
||||||
|
|
||||||
# 5.0.4 (2018-03-28)
|
# 5.0.4 (2018-03-28)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ var (
|
|||||||
includeBuildNumber bool = true
|
includeBuildNumber bool = true
|
||||||
buildNumber int = 0
|
buildNumber int = 0
|
||||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||||
|
isDev bool = false
|
||||||
)
|
)
|
||||||
|
|
||||||
const minGoVersion = 1.8
|
const minGoVersion = 1.8
|
||||||
@@ -61,6 +62,7 @@ func main() {
|
|||||||
flag.BoolVar(&race, "race", race, "Use race detector")
|
flag.BoolVar(&race, "race", race, "Use race detector")
|
||||||
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
|
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
|
||||||
flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
|
flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
|
||||||
|
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
readVersionFromPackageJson()
|
readVersionFromPackageJson()
|
||||||
@@ -394,7 +396,9 @@ func build(binaryName, pkg string, tags []string) {
|
|||||||
binary += ".exe"
|
binary += ".exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
rmr(binary, binary+".md5")
|
if !isDev {
|
||||||
|
rmr(binary, binary+".md5")
|
||||||
|
}
|
||||||
args := []string{"build", "-ldflags", ldflags()}
|
args := []string{"build", "-ldflags", ldflags()}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
args = append(args, "-tags", strings.Join(tags, ","))
|
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, "-o", binary)
|
||||||
args = append(args, pkg)
|
args = append(args, pkg)
|
||||||
setBuildEnv()
|
|
||||||
|
|
||||||
runPrint("go", "version")
|
if !isDev {
|
||||||
|
setBuildEnv()
|
||||||
|
runPrint("go", "version")
|
||||||
|
}
|
||||||
|
|
||||||
runPrint("go", args...)
|
runPrint("go", args...)
|
||||||
|
|
||||||
// Create an md5 checksum of the binary, to be included in the archive for
|
if !isDev {
|
||||||
// automatic upgrades.
|
// Create an md5 checksum of the binary, to be included in the archive for
|
||||||
err := md5File(binary)
|
// automatic upgrades.
|
||||||
if err != nil {
|
err := md5File(binary)
|
||||||
log.Fatal(err)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +444,10 @@ func rmr(paths ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func clean() {
|
func clean() {
|
||||||
|
if isDev {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rmr("dist")
|
rmr("dist")
|
||||||
rmr("tmp")
|
rmr("tmp")
|
||||||
rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/grafana", goos, goarch)))
|
rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/grafana", goos, goarch)))
|
||||||
@@ -550,7 +563,7 @@ func shaFilesInDist() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(path, ".sha256") == false {
|
if !strings.Contains(path, ".sha256") {
|
||||||
err := shaFile(path)
|
err := shaFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create sha file. error: %v\n", err)
|
log.Printf("Failed to create sha file. error: %v\n", err)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ are supported.
|
|||||||
|
|
||||||
### Min time interval
|
### 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.
|
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:
|
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
|
||||||
|
|
||||||
Identifier | Description
|
Identifier | Description
|
||||||
@@ -178,4 +178,4 @@ datasources:
|
|||||||
jsonData:
|
jsonData:
|
||||||
interval: Daily
|
interval: Daily
|
||||||
timeField: "@timestamp"
|
timeField: "@timestamp"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ All requests will be made from the browser directly to the data source and may b
|
|||||||
|
|
||||||
### Min time interval
|
### 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.
|
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:
|
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
|
||||||
|
|
||||||
Identifier | Description
|
Identifier | Description
|
||||||
@@ -212,4 +212,4 @@ datasources:
|
|||||||
user: grafana
|
user: grafana
|
||||||
password: grafana
|
password: grafana
|
||||||
url: http://localhost:8086
|
url: http://localhost:8086
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
|||||||
"defaultRegion": "us-west-1"
|
"defaultRegion": "us-west-1"
|
||||||
},
|
},
|
||||||
"secureJsonData": {
|
"secureJsonData": {
|
||||||
"accessKey": "Ol4pIDpeKSA6XikgOl4p",
|
"accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
|
||||||
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
|
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -258,9 +258,6 @@ func (this *thunderTask) fetch() error {
|
|||||||
this.Avatar.data = &bytes.Buffer{}
|
this.Avatar.data = &bytes.Buffer{}
|
||||||
writer := bufio.NewWriter(this.Avatar.data)
|
writer := bufio.NewWriter(this.Avatar.data)
|
||||||
|
|
||||||
if _, err = io.Copy(writer, resp.Body); err != nil {
|
_, err = io.Copy(writer, resp.Body)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hs.httpSrv.TLSConfig = tlsCfg
|
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)
|
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -101,13 +101,14 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
|
|||||||
return Error(401, "Login is disabled", nil)
|
return Error(401, "Login is disabled", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
authQuery := login.LoginUserQuery{
|
authQuery := &m.LoginUserQuery{
|
||||||
Username: cmd.User,
|
ReqContext: c,
|
||||||
Password: cmd.Password,
|
Username: cmd.User,
|
||||||
IpAddress: c.Req.RemoteAddr,
|
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 {
|
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
|
||||||
return Error(401, "Invalid username or password", err)
|
return Error(401, "Invalid username or password", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-46
@@ -6,7 +6,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -16,22 +15,15 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/login"
|
||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
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/services/session"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/social"
|
"github.com/grafana/grafana/pkg/social"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var oauthLogger = log.New("oauth")
|
||||||
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenStateString() string {
|
func GenStateString() string {
|
||||||
rnd := make([]byte, 32)
|
rnd := make([]byte, 32)
|
||||||
@@ -56,7 +48,7 @@ func OAuthLogin(ctx *m.ReqContext) {
|
|||||||
if errorParam != "" {
|
if errorParam != "" {
|
||||||
errorDesc := ctx.Query("error_description")
|
errorDesc := ctx.Query("error_description")
|
||||||
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,54 +141,43 @@ func OAuthLogin(ctx *m.ReqContext) {
|
|||||||
|
|
||||||
// validate that we got at least an email address
|
// validate that we got at least an email address
|
||||||
if userInfo.Email == "" {
|
if userInfo.Email == "" {
|
||||||
redirectWithError(ctx, ErrNoEmail)
|
redirectWithError(ctx, login.ErrNoEmail)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate that the email is allowed to login to grafana
|
// validate that the email is allowed to login to grafana
|
||||||
if !connect.IsEmailAllowed(userInfo.Email) {
|
if !connect.IsEmailAllowed(userInfo.Email) {
|
||||||
redirectWithError(ctx, ErrEmailNotAllowed)
|
redirectWithError(ctx, login.ErrEmailNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userQuery := m.GetUserByEmailQuery{Email: userInfo.Email}
|
extUser := &m.ExternalUserInfo{
|
||||||
err = bus.Dispatch(&userQuery)
|
AuthModule: "oauth_" + name,
|
||||||
|
AuthId: userInfo.Id,
|
||||||
|
Name: userInfo.Name,
|
||||||
|
Login: userInfo.Login,
|
||||||
|
Email: userInfo.Email,
|
||||||
|
OrgRoles: map[int64]m.RoleType{},
|
||||||
|
}
|
||||||
|
|
||||||
// create account if missing
|
if userInfo.Role != "" {
|
||||||
if err == m.ErrUserNotFound {
|
extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
|
||||||
if !connect.IsSignupAllowed() {
|
}
|
||||||
redirectWithError(ctx, ErrSignUpNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
limitReached, err := quota.QuotaReached(ctx, "user")
|
|
||||||
if err != nil {
|
|
||||||
ctx.Handle(500, "Failed to get user quota", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if limitReached {
|
|
||||||
redirectWithError(ctx, ErrUsersQuotaReached)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cmd := m.CreateUserCommand{
|
|
||||||
Login: userInfo.Login,
|
|
||||||
Email: userInfo.Email,
|
|
||||||
Name: userInfo.Name,
|
|
||||||
Company: userInfo.Company,
|
|
||||||
DefaultOrgRole: userInfo.Role,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = bus.Dispatch(&cmd); err != nil {
|
// add/update user in grafana
|
||||||
ctx.Handle(500, "Failed to create account", err)
|
cmd := &m.UpsertUserCommand{
|
||||||
return
|
ReqContext: ctx,
|
||||||
}
|
ExternalUser: extUser,
|
||||||
|
SignupAllowed: connect.IsSignupAllowed(),
|
||||||
userQuery.Result = &cmd.Result
|
}
|
||||||
} else if err != nil {
|
err = bus.Dispatch(cmd)
|
||||||
ctx.Handle(500, "Unexpected error", err)
|
if err != nil {
|
||||||
|
redirectWithError(ctx, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// login
|
// login
|
||||||
loginUserWithUser(userQuery.Result, ctx)
|
loginUserWithUser(cmd.Result, ctx)
|
||||||
|
|
||||||
metrics.M_Api_Login_OAuth.Inc()
|
metrics.M_Api_Login_OAuth.Inc()
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
|
|||||||
fileInfo, err := os.Stat(pluginsDir)
|
fileInfo, err := os.Stat(pluginsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err = os.MkdirAll(pluginsDir, os.ModePerm); 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
|
|||||||
return fmt.Errorf("error: %s", err)
|
return fmt.Errorf("error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pluginDirInfo.IsDir() == false {
|
if !pluginDirInfo.IsDir() {
|
||||||
return errors.New("plugin path is not a directory")
|
return errors.New("plugin path is not a directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
|
|||||||
for _, p := range pluginsToUpgrade {
|
for _, p := range pluginsToUpgrade {
|
||||||
logger.Infof("Updating %v \n", p.Id)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,7 +585,6 @@ func (v *Value) Null() error {
|
|||||||
switch v.data.(type) {
|
switch v.data.(type) {
|
||||||
case nil:
|
case nil:
|
||||||
valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
|
valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
@@ -607,7 +606,6 @@ func (v *Value) Array() ([]*Value, error) {
|
|||||||
switch v.data.(type) {
|
switch v.data.(type) {
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
valid = true
|
valid = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsure if this is a good way to use slices, it's probably not
|
// 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) {
|
switch v.data.(type) {
|
||||||
case json.Number:
|
case json.Number:
|
||||||
valid = true
|
valid = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
@@ -687,7 +684,6 @@ func (v *Value) Boolean() (bool, error) {
|
|||||||
switch v.data.(type) {
|
switch v.data.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
valid = true
|
valid = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
@@ -709,7 +705,6 @@ func (v *Value) Object() (*Object, error) {
|
|||||||
switch v.data.(type) {
|
switch v.data.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
valid = true
|
valid = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
@@ -746,7 +741,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
|
|||||||
switch v.data.(type) {
|
switch v.data.(type) {
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
valid = true
|
valid = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsure if this is a good way to use slices, it's probably not
|
// 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) {
|
switch v.data.(type) {
|
||||||
case string:
|
case string:
|
||||||
valid = true
|
valid = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (assert *Assert) True(value bool, message string) {
|
func (assert *Assert) True(value bool, message string) {
|
||||||
if value == false {
|
if !value {
|
||||||
log.Panicln("Assert: ", message)
|
log.Panicln("Assert: ", message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,13 +119,13 @@ func TestFirst(t *testing.T) {
|
|||||||
assert.True(s == "" && err != nil, "nonexistent string fail")
|
assert.True(s == "" && err != nil, "nonexistent string fail")
|
||||||
|
|
||||||
b, err := j.GetBoolean("true")
|
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")
|
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")
|
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")
|
list, err := j.GetValueArray("list")
|
||||||
assert.True(list != nil && err == nil, "list should be an array")
|
assert.True(list != nil && err == nil, "list should be an array")
|
||||||
|
|||||||
+1
-4
@@ -99,10 +99,7 @@ func (w *FileLogWriter) StartLogger() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.mw.SetFd(fd)
|
w.mw.SetFd(fd)
|
||||||
if err = w.initFd(); err != nil {
|
return w.initFd()
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *FileLogWriter) docheck(size int) {
|
func (w *FileLogWriter) docheck(size int) {
|
||||||
|
|||||||
+9
-10
@@ -8,23 +8,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||||
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
|
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() {
|
func Init() {
|
||||||
bus.AddHandler("auth", AuthenticateUser)
|
bus.AddHandler("auth", AuthenticateUser)
|
||||||
loadLdapConfig()
|
loadLdapConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthenticateUser(query *LoginUserQuery) error {
|
func AuthenticateUser(query *m.LoginUserQuery) error {
|
||||||
if err := validateLoginAttempts(query.Username); err != nil {
|
if err := validateLoginAttempts(query.Username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func TestAuthenticateUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type authScenarioContext struct {
|
type authScenarioContext struct {
|
||||||
loginUserQuery *LoginUserQuery
|
loginUserQuery *m.LoginUserQuery
|
||||||
grafanaLoginWasCalled bool
|
grafanaLoginWasCalled bool
|
||||||
ldapLoginWasCalled bool
|
ldapLoginWasCalled bool
|
||||||
loginAttemptValidationWasCalled bool
|
loginAttemptValidationWasCalled bool
|
||||||
@@ -161,14 +161,14 @@ type authScenarioContext struct {
|
|||||||
type authScenarioFunc func(sc *authScenarioContext)
|
type authScenarioFunc func(sc *authScenarioContext)
|
||||||
|
|
||||||
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
|
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
|
||||||
loginUsingGrafanaDB = func(query *LoginUserQuery) error {
|
loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
|
||||||
sc.grafanaLoginWasCalled = true
|
sc.grafanaLoginWasCalled = true
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
|
func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
|
||||||
loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
|
loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
|
||||||
sc.ldapLoginWasCalled = true
|
sc.ldapLoginWasCalled = true
|
||||||
return enabled, err
|
return enabled, err
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
|
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
|
||||||
saveInvalidLoginAttempt = func(query *LoginUserQuery) {
|
saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
|
||||||
sc.saveInvalidLoginAttemptWasCalled = true
|
sc.saveInvalidLoginAttemptWasCalled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ func authScenario(desc string, fn authScenarioFunc) {
|
|||||||
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
|
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
|
||||||
|
|
||||||
sc := &authScenarioContext{
|
sc := &authScenarioContext{
|
||||||
loginUserQuery: &LoginUserQuery{
|
loginUserQuery: &m.LoginUserQuery{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "pwd",
|
Password: "pwd",
|
||||||
IpAddress: "192.168.1.1:56433",
|
IpAddress: "192.168.1.1:56433",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ var validateLoginAttempts = func(username string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var saveInvalidLoginAttempt = func(query *LoginUserQuery) {
|
var saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
|
||||||
if setting.DisableBruteForceLoginProtection {
|
if setting.DisableBruteForceLoginProtection {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
saveInvalidLoginAttempt(&LoginUserQuery{
|
saveInvalidLoginAttempt(&m.LoginUserQuery{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "pwd",
|
Password: "pwd",
|
||||||
IpAddress: "192.168.1.1:56433",
|
IpAddress: "192.168.1.1:56433",
|
||||||
@@ -103,7 +103,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
saveInvalidLoginAttempt(&LoginUserQuery{
|
saveInvalidLoginAttempt(&m.LoginUserQuery{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "pwd",
|
Password: "pwd",
|
||||||
IpAddress: "192.168.1.1:56433",
|
IpAddress: "192.168.1.1:56433",
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginUsingGrafanaDB = func(query *LoginUserQuery) error {
|
var loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
|
||||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
|
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
|
||||||
|
|
||||||
if err := bus.Dispatch(&userQuery); err != nil {
|
if err := bus.Dispatch(&userQuery); err != nil {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func TestGrafanaLogin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type grafanaLoginScenarioContext struct {
|
type grafanaLoginScenarioContext struct {
|
||||||
loginUserQuery *LoginUserQuery
|
loginUserQuery *m.LoginUserQuery
|
||||||
validatePasswordCalled bool
|
validatePasswordCalled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) {
|
|||||||
origValidatePassword := validatePassword
|
origValidatePassword := validatePassword
|
||||||
|
|
||||||
sc := &grafanaLoginScenarioContext{
|
sc := &grafanaLoginScenarioContext{
|
||||||
loginUserQuery: &LoginUserQuery{
|
loginUserQuery: &m.LoginUserQuery{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "pwd",
|
Password: "pwd",
|
||||||
IpAddress: "192.168.1.1:56433",
|
IpAddress: "192.168.1.1:56433",
|
||||||
|
|||||||
+75
-181
@@ -24,10 +24,9 @@ type ILdapConn interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ILdapAuther interface {
|
type ILdapAuther interface {
|
||||||
Login(query *LoginUserQuery) error
|
Login(query *m.LoginUserQuery) error
|
||||||
SyncSignedInUser(signedInUser *m.SignedInUser) error
|
SyncUser(query *m.LoginUserQuery) error
|
||||||
GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
|
GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
|
||||||
SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ldapAuther struct {
|
type ldapAuther struct {
|
||||||
@@ -89,7 +88,8 @@ func (a *ldapAuther) Dial() error {
|
|||||||
return err
|
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 {
|
if err := a.Dial(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -101,206 +101,105 @@ func (a *ldapAuther) Login(query *LoginUserQuery) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// find user entry & attributes
|
// find user entry & attributes
|
||||||
if ldapUser, err := a.searchForUser(query.Username); err != nil {
|
ldapUser, err := a.searchForUser(query.Username)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
}
|
||||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
|
||||||
|
|
||||||
// check if a second user bind is needed
|
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||||
if a.requireSecondBind {
|
|
||||||
if err := a.secondBind(ldapUser, query.Password); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return err
|
||||||
} else {
|
|
||||||
if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil {
|
|
||||||
return syncErr
|
|
||||||
}
|
|
||||||
query.User = grafanaUser
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.User = grafanaUser
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ldapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error {
|
||||||
grafanaUser := m.User{
|
// connect to ldap server
|
||||||
Id: signedInUser.UserId,
|
err := a.Dial()
|
||||||
Login: signedInUser.Login,
|
if err != nil {
|
||||||
Email: signedInUser.Email,
|
|
||||||
Name: signedInUser.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.Dial(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer a.conn.Close()
|
defer a.conn.Close()
|
||||||
if err := a.serverBind(); err != nil {
|
|
||||||
|
err = a.serverBind()
|
||||||
|
if err != nil {
|
||||||
return err
|
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)
|
a.log.Error("Failed searching for user in ldap", "error", err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
} else {
|
}
|
||||||
if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil {
|
|
||||||
return err
|
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||||
|
|
||||||
|
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.User = grafanaUser
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
|
||||||
|
extUser := &m.ExternalUserInfo{
|
||||||
|
AuthModule: "ldap",
|
||||||
|
AuthId: ldapUser.DN,
|
||||||
|
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
|
||||||
|
Login: ldapUser.Username,
|
||||||
|
Email: ldapUser.Email,
|
||||||
|
OrgRoles: map[int64]m.RoleType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range a.server.LdapGroups {
|
||||||
|
// only use the first match for each org
|
||||||
|
if extUser.OrgRoles[group.OrgId] != "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser))
|
if ldapUser.isMemberOf(group.GroupDN) {
|
||||||
|
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync info for ldap user and grafana user
|
|
||||||
func (a *ldapAuther) syncInfoAndOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
|
||||||
// sync user details
|
|
||||||
if err := a.syncUserInfo(user, ldapUser); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// sync org roles
|
|
||||||
if err := a.SyncOrgRoles(user, ldapUser); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
|
|
||||||
// validate that the user has access
|
// validate that the user has access
|
||||||
// if there are no ldap group mappings access is true
|
// if there are no ldap group mappings access is true
|
||||||
// otherwise a single group must match
|
// otherwise a single group must match
|
||||||
access := len(a.server.LdapGroups) == 0
|
if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 {
|
||||||
for _, ldapGroup := range a.server.LdapGroups {
|
a.log.Info(
|
||||||
if ldapUser.isMemberOf(ldapGroup.GroupDN) {
|
"Ldap Auth: user does not belong in any of the specified ldap groups",
|
||||||
access = true
|
"username", ldapUser.Username,
|
||||||
break
|
"groups", ldapUser.MemberOf)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user from grafana db
|
// add/update user in grafana
|
||||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username}
|
userQuery := &m.UpsertUserCommand{
|
||||||
if err := bus.Dispatch(&userQuery); err != nil {
|
ReqContext: ctx,
|
||||||
if err == m.ErrUserNotFound && setting.LdapAllowSignup {
|
ExternalUser: extUser,
|
||||||
return a.createGrafanaUser(ldapUser)
|
SignupAllowed: setting.LdapAllowSignup,
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
err := bus.Dispatch(userQuery)
|
||||||
return userQuery.Result, nil
|
if err != 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 nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cmd.Result, nil
|
return userQuery.Result, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *LdapUserInfo) error {
|
|
||||||
var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
|
|
||||||
if user.Email == ldapUser.Email && user.Name == name {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
a.log.Debug("Syncing user info", "username", ldapUser.Username)
|
|
||||||
updateCmd := m.UpdateUserCommand{}
|
|
||||||
updateCmd.UserId = user.Id
|
|
||||||
updateCmd.Login = user.Login
|
|
||||||
updateCmd.Email = ldapUser.Email
|
|
||||||
updateCmd.Name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
|
|
||||||
return bus.Dispatch(&updateCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ldapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
|
||||||
if len(a.server.LdapGroups) == 0 {
|
|
||||||
a.log.Warn("No group mappings defined")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
|
|
||||||
if err := bus.Dispatch(&orgsQuery); err != nil {
|
|
||||||
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) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, exists := handledOrgIds[group.OrgId]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// add role
|
|
||||||
cmd := m.AddOrgUserCommand{UserId: user.Id, Role: group.OrgRole, OrgId: group.OrgId}
|
|
||||||
err := bus.Dispatch(&cmd)
|
|
||||||
if err != nil && err != m.ErrOrgNotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// mark this group has handled so we do not process it again
|
|
||||||
handledOrgIds[group.OrgId] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ldapAuther) serverBind() error {
|
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
|
// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
|
||||||
var groupSearchResult *ldap.SearchResult
|
var groupSearchResult *ldap.SearchResult
|
||||||
for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
|
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 == "" {
|
if a.server.GroupSearchFilterUserAttribute == "" {
|
||||||
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
|
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
|
||||||
}
|
}
|
||||||
@@ -470,7 +368,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
|
|||||||
}
|
}
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUserFromLdapInfo() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package login
|
package login
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
var loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
|
var loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
|
||||||
if !setting.LdapEnabled {
|
if !setting.LdapEnabled {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func TestLdapLogin(t *testing.T) {
|
|||||||
|
|
||||||
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
|
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
|
||||||
sc.withLoginResult(false)
|
sc.withLoginResult(false)
|
||||||
enabled, err := loginUsingLdap(&LoginUserQuery{
|
enabled, err := loginUsingLdap(&m.LoginUserQuery{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "pwd",
|
Password: "pwd",
|
||||||
})
|
})
|
||||||
@@ -117,7 +117,7 @@ type mockLdapAuther struct {
|
|||||||
loginCalled bool
|
loginCalled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
|
func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error {
|
||||||
a.loginCalled = true
|
a.loginCalled = true
|
||||||
|
|
||||||
if !a.validLogin {
|
if !a.validLogin {
|
||||||
@@ -127,20 +127,16 @@ func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error {
|
||||||
return nil
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ldapLoginScenarioContext struct {
|
type ldapLoginScenarioContext struct {
|
||||||
loginUserQuery *LoginUserQuery
|
loginUserQuery *m.LoginUserQuery
|
||||||
ldapAuthenticatorMock *mockLdapAuther
|
ldapAuthenticatorMock *mockLdapAuther
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +147,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
|
|||||||
origNewLdapAuthenticator := NewLdapAuthenticator
|
origNewLdapAuthenticator := NewLdapAuthenticator
|
||||||
|
|
||||||
sc := &ldapLoginScenarioContext{
|
sc := &ldapLoginScenarioContext{
|
||||||
loginUserQuery: &LoginUserQuery{
|
loginUserQuery: &m.LoginUserQuery{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "pwd",
|
Password: "pwd",
|
||||||
IpAddress: "192.168.1.1:56433",
|
IpAddress: "192.168.1.1:56433",
|
||||||
|
|||||||
+62
-31
@@ -18,7 +18,7 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||||
LdapGroups: []*LdapGroupToOrgRole{{}},
|
LdapGroups: []*LdapGroupToOrgRole{{}},
|
||||||
})
|
})
|
||||||
_, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
|
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
|
||||||
|
|
||||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||||
})
|
})
|
||||||
@@ -34,7 +34,7 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
|
|
||||||
sc.userQueryReturns(user1)
|
sc.userQueryReturns(user1)
|
||||||
|
|
||||||
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
|
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(result, ShouldEqual, user1)
|
So(result, ShouldEqual, user1)
|
||||||
})
|
})
|
||||||
@@ -48,7 +48,7 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
|
|
||||||
sc.userQueryReturns(user1)
|
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(err, ShouldBeNil)
|
||||||
So(result, ShouldEqual, user1)
|
So(result, ShouldEqual, user1)
|
||||||
})
|
})
|
||||||
@@ -64,7 +64,8 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
|
|
||||||
sc.userQueryReturns(nil)
|
sc.userQueryReturns(nil)
|
||||||
|
|
||||||
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{
|
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||||
|
DN: "torkelo",
|
||||||
Username: "torkelo",
|
Username: "torkelo",
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
MemberOf: []string{"cn=editor"},
|
MemberOf: []string{"cn=editor"},
|
||||||
@@ -72,11 +73,6 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
|
|
||||||
So(err, ShouldBeNil)
|
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() {
|
Convey("Should return new user", func() {
|
||||||
So(result.Login, ShouldEqual, "torkelo")
|
So(result.Login, ShouldEqual, "torkelo")
|
||||||
})
|
})
|
||||||
@@ -95,7 +91,7 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||||
MemberOf: []string{"cn=users"},
|
MemberOf: []string{"cn=users"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -114,7 +110,7 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
|
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"},
|
MemberOf: []string{"cn=users"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,24 +118,29 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(sc.updateOrgUserCmd, ShouldNotBeNil)
|
So(sc.updateOrgUserCmd, ShouldNotBeNil)
|
||||||
So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
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) {
|
ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
|
||||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||||
LdapGroups: []*LdapGroupToOrgRole{
|
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}})
|
sc.userOrgsQueryReturns([]*m.UserOrgDTO{
|
||||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
{OrgId: 1, Role: m.ROLE_EDITOR},
|
||||||
MemberOf: []string{"cn=other"},
|
{OrgId: 2, Role: m.ROLE_EDITOR},
|
||||||
|
})
|
||||||
|
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||||
|
MemberOf: []string{"cn=users"},
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should remove org role", func() {
|
Convey("Should remove org role", func() {
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(sc.removeOrgUserCmd, ShouldNotBeNil)
|
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}})
|
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"},
|
MemberOf: []string{"cn=users"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -160,6 +161,7 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(sc.removeOrgUserCmd, ShouldBeNil)
|
So(sc.removeOrgUserCmd, ShouldBeNil)
|
||||||
So(sc.updateOrgUserCmd, ShouldNotBeNil)
|
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}})
|
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"},
|
MemberOf: []string{"cn=admins"},
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should take first match, and ignore subsequent matches", func() {
|
Convey("Should take first match, and ignore subsequent matches", func() {
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(sc.updateOrgUserCmd, ShouldBeNil)
|
So(sc.updateOrgUserCmd, ShouldBeNil)
|
||||||
|
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,19 +194,20 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||||
MemberOf: []string{"cn=admins"},
|
MemberOf: []string{"cn=admins"},
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should take first match, and ignore subsequent matches", func() {
|
Convey("Should take first match, and ignore subsequent matches", func() {
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
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{}
|
mockLdapConnection := &mockLdapConn{}
|
||||||
ldapAuther := NewLdapAuthenticator(
|
ldapAuther := NewLdapAuthenticator(
|
||||||
@@ -243,17 +247,20 @@ func TestLdapAuther(t *testing.T) {
|
|||||||
|
|
||||||
ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
|
ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
|
||||||
// arrange
|
// arrange
|
||||||
signedInUser := &m.SignedInUser{
|
query := &m.LoginUserQuery{
|
||||||
Email: "roel@test.net",
|
Username: "roelgerrits",
|
||||||
UserId: 1,
|
|
||||||
Name: "Roel Gerrits",
|
|
||||||
Login: "roelgerrits",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sc.userQueryReturns(&m.User{
|
||||||
|
Id: 1,
|
||||||
|
Email: "roel@test.net",
|
||||||
|
Name: "Roel Gerrits",
|
||||||
|
Login: "roelgerrits",
|
||||||
|
})
|
||||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||||
|
|
||||||
// act
|
// act
|
||||||
syncErrResult := ldapAuther.SyncSignedInUser(signedInUser)
|
syncErrResult := ldapAuther.SyncUser(query)
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
So(dialCalled, ShouldBeTrue)
|
So(dialCalled, ShouldBeTrue)
|
||||||
@@ -299,6 +306,19 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
|
|||||||
|
|
||||||
sc := &scenarioContext{}
|
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 {
|
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
|
||||||
sc.createUserCmd = cmd
|
sc.createUserCmd = cmd
|
||||||
sc.createUserCmd.Result = m.User{Login: cmd.Login}
|
sc.createUserCmd.Result = m.User{Login: cmd.Login}
|
||||||
@@ -325,20 +345,28 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error {
|
||||||
|
sc.setUsingOrgCmd = cmd
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
fn(sc)
|
fn(sc)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type scenarioContext struct {
|
type scenarioContext struct {
|
||||||
createUserCmd *m.CreateUserCommand
|
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
|
||||||
addOrgUserCmd *m.AddOrgUserCommand
|
getUserOrgListQuery *m.GetUserOrgListQuery
|
||||||
updateOrgUserCmd *m.UpdateOrgUserCommand
|
createUserCmd *m.CreateUserCommand
|
||||||
removeOrgUserCmd *m.RemoveOrgUserCommand
|
addOrgUserCmd *m.AddOrgUserCommand
|
||||||
updateUserCmd *m.UpdateUserCommand
|
updateOrgUserCmd *m.UpdateOrgUserCommand
|
||||||
|
removeOrgUserCmd *m.RemoveOrgUserCommand
|
||||||
|
updateUserCmd *m.UpdateUserCommand
|
||||||
|
setUsingOrgCmd *m.SetUsingOrgCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *scenarioContext) userQueryReturns(user *m.User) {
|
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 {
|
if user == nil {
|
||||||
return m.ErrUserNotFound
|
return m.ErrUserNotFound
|
||||||
} else {
|
} else {
|
||||||
@@ -346,6 +374,9 @@ func (sc *scenarioContext) userQueryReturns(user *m.User) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *scenarioContext) userOrgsQueryReturns(orgs []*m.UserOrgDTO) {
|
func (sc *scenarioContext) userOrgsQueryReturns(orgs []*m.UserOrgDTO) {
|
||||||
|
|||||||
@@ -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 addExtentionConventionForRollups(buf, mf, m)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {
|
func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {
|
||||||
|
|||||||
+104
-72
@@ -3,6 +3,7 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
|
||||||
|
|
||||||
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||||
if !setting.AuthProxyEnabled {
|
if !setting.AuthProxyEnabled {
|
||||||
return false
|
return false
|
||||||
@@ -30,40 +33,104 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
|||||||
return true
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if !setting.AuthProxyAutoSignUp {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
|
|
||||||
if setting.LdapEnabled {
|
|
||||||
cmd.SkipOrgSetup = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bus.Dispatch(cmd); err != nil {
|
|
||||||
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
|
|
||||||
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)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize session
|
// initialize session
|
||||||
if err := ctx.Session.Start(ctx.Context); err != nil {
|
if err := ctx.Session.Start(ctx.Context); err != nil {
|
||||||
log.Error(3, "Failed to start session", err)
|
log.Error(3, "Failed to start session", err)
|
||||||
return false
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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!
|
// Make sure that we cannot share a session between different users!
|
||||||
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
|
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
|
||||||
// remove session
|
// remove session
|
||||||
@@ -77,16 +144,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When ldap is enabled, sync userinfo and org roles
|
ctx.Session.Set(AUTH_PROXY_SESSION_VAR, proxyHeaderValue)
|
||||||
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.SignedInUser = query.Result
|
ctx.SignedInUser = query.Result
|
||||||
ctx.IsSignedIn = true
|
ctx.IsSignedIn = true
|
||||||
@@ -95,29 +153,29 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
|
var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
|
||||||
if !setting.LdapEnabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
|
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
|
||||||
|
|
||||||
var lastLdapSync int64
|
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)
|
lastLdapSync = lastLdapSyncInSession.(int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastLdapSync < expireEpoch {
|
if lastLdapSync < expireEpoch {
|
||||||
ldapCfg := login.LdapCfg
|
ldapCfg := login.LdapCfg
|
||||||
|
|
||||||
|
if len(ldapCfg.Servers) < 1 {
|
||||||
|
return fmt.Errorf("No LDAP servers available")
|
||||||
|
}
|
||||||
|
|
||||||
for _, server := range ldapCfg.Servers {
|
for _, server := range ldapCfg.Servers {
|
||||||
author := login.NewLdapAuthenticator(server)
|
author := login.NewLdapAuthenticator(server)
|
||||||
if err := author.SyncSignedInUser(query.Result); err != nil {
|
if err := author.SyncUser(query); err != nil {
|
||||||
return err
|
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
|
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)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,57 +26,71 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) {
|
|||||||
return &mockLdapAuther
|
return &mockLdapAuther
|
||||||
}
|
}
|
||||||
|
|
||||||
signedInUser := m.SignedInUser{}
|
Convey("When user logs in, call SyncUser", func() {
|
||||||
query := m.GetSignedInUserQuery{Result: &signedInUser}
|
|
||||||
|
|
||||||
Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() {
|
|
||||||
// arrange
|
// arrange
|
||||||
sess := mockSession{}
|
sess := newMockSession()
|
||||||
ctx := m.ReqContext{Session: &sess}
|
ctx := m.ReqContext{Session: &sess}
|
||||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
|
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
|
||||||
|
|
||||||
// act
|
// act
|
||||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
|
||||||
|
ReqContext: &ctx,
|
||||||
|
Username: "test",
|
||||||
|
})
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
|
So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
|
||||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0)
|
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() {
|
Convey("When session variable not expired, don't sync and don't change session var", func() {
|
||||||
// arrange
|
// arrange
|
||||||
sess := mockSession{}
|
sess := newMockSession()
|
||||||
ctx := m.ReqContext{Session: &sess}
|
ctx := m.ReqContext{Session: &sess}
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
sess.Set(session.SESS_KEY_LASTLDAPSYNC, now)
|
sess.Set(session.SESS_KEY_LASTLDAPSYNC, now)
|
||||||
|
sess.Set(AUTH_PROXY_SESSION_VAR, "test")
|
||||||
|
|
||||||
// act
|
// act
|
||||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
|
||||||
|
ReqContext: &ctx,
|
||||||
|
Username: "test",
|
||||||
|
})
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
|
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() {
|
Convey("When lastldapsync is expired, session variable should be updated", func() {
|
||||||
// arrange
|
// arrange
|
||||||
sess := mockSession{}
|
sess := newMockSession()
|
||||||
ctx := m.ReqContext{Session: &sess}
|
ctx := m.ReqContext{Session: &sess}
|
||||||
expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
|
expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
|
||||||
sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime)
|
sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime)
|
||||||
|
sess.Set(AUTH_PROXY_SESSION_VAR, "test")
|
||||||
|
|
||||||
// act
|
// act
|
||||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
|
||||||
|
ReqContext: &ctx,
|
||||||
|
Username: "test",
|
||||||
|
})
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
|
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
|
||||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
|
So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockSession struct {
|
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 {
|
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 {
|
func (s *mockSession) Set(k interface{}, v interface{}) error {
|
||||||
s.value = v
|
s.value[k] = v
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mockSession) Get(k interface{}) interface{} {
|
func (s *mockSession) Get(k interface{}) interface{} {
|
||||||
return s.value
|
return s.value[k]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mockSession) Delete(k interface{}) interface{} {
|
func (s *mockSession) Delete(k interface{}) interface{} {
|
||||||
|
delete(s.value, k)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,21 +128,18 @@ func (s *mockSession) RegenerateId(c *macaron.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mockLdapAuthenticator struct {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *mockLdapAuthenticator) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
func (a *mockLdapAuthenticator) SyncUser(query *m.LoginUserQuery) error {
|
||||||
a.syncSignedInUserCalled = true
|
a.syncUserCalled = true
|
||||||
return nil
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (a *mockLdapAuthenticator) SyncOrgRoles(user *m.User, ldapUser *login.LdapUserInfo) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
l "github.com/grafana/grafana/pkg/login"
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/session"
|
"github.com/grafana/grafana/pkg/services/session"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -165,7 +164,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
|||||||
|
|
||||||
user := loginQuery.Result
|
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 {
|
if err := bus.Dispatch(&loginUserQuery); err != nil {
|
||||||
ctx.JsonApiErr(401, "Invalid username or password", err)
|
ctx.JsonApiErr(401, "Invalid username or password", err)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
ms "github.com/go-macaron/session"
|
ms "github.com/go-macaron/session"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
l "github.com/grafana/grafana/pkg/login"
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/session"
|
"github.com/grafana/grafana/pkg/services/session"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -72,7 +71,7 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(loginUserQuery *l.LoginUserQuery) error {
|
bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -177,12 +176,18 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.AuthProxyEnabled = true
|
setting.AuthProxyEnabled = true
|
||||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
setting.AuthProxyHeaderProperty = "username"
|
setting.AuthProxyHeaderProperty = "username"
|
||||||
|
setting.LdapEnabled = false
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||||
|
cmd.Result = &m.User{Id: 12}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
sc.fakeReq("GET", "/")
|
sc.fakeReq("GET", "/")
|
||||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||||
sc.exec()
|
sc.exec()
|
||||||
@@ -199,6 +204,7 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
setting.AuthProxyHeaderProperty = "username"
|
setting.AuthProxyHeaderProperty = "username"
|
||||||
setting.AuthProxyAutoSignUp = true
|
setting.AuthProxyAutoSignUp = true
|
||||||
|
setting.LdapEnabled = false
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||||
if query.UserId > 0 {
|
if query.UserId > 0 {
|
||||||
@@ -209,8 +215,8 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
|
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||||
cmd.Result = m.User{Id: 33}
|
cmd.Result = &m.User{Id: 33}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -271,6 +277,11 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||||
|
cmd.Result = &m.User{Id: 33}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
sc.fakeReq("GET", "/")
|
sc.fakeReq("GET", "/")
|
||||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||||
sc.req.RemoteAddr = "[2001::23]:12345"
|
sc.req.RemoteAddr = "[2001::23]:12345"
|
||||||
@@ -289,6 +300,11 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.AuthProxyHeaderProperty = "username"
|
setting.AuthProxyHeaderProperty = "username"
|
||||||
setting.AuthProxyWhitelist = ""
|
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 {
|
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
||||||
return nil
|
return nil
|
||||||
@@ -319,11 +335,17 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.LdapEnabled = true
|
setting.LdapEnabled = true
|
||||||
|
|
||||||
called := false
|
called := false
|
||||||
syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
|
syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
|
||||||
called = true
|
called = true
|
||||||
|
query.User = &m.User{Id: 32}
|
||||||
return nil
|
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 {
|
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ func (r *RoleType) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
*r = RoleType(str)
|
*r = RoleType(str)
|
||||||
|
|
||||||
if (*r).IsValid() == false {
|
if !(*r).IsValid() {
|
||||||
if (*r) != "" {
|
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
|
*r = ROLE_VIEWER
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ func TestMappingRowValue(t *testing.T) {
|
|||||||
|
|
||||||
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
|
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
|
||||||
haveBool, ok := boolRowValue.(bool)
|
haveBool, ok := boolRowValue.(bool)
|
||||||
if !ok || haveBool != true {
|
if !ok || !haveBool {
|
||||||
t.Fatalf("Expected true, was %v", haveBool)
|
t.Fatalf("Expected true, was %v", haveBool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type AlertEvaluator interface {
|
|||||||
type NoValueEvaluator struct{}
|
type NoValueEvaluator struct{}
|
||||||
|
|
||||||
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
|
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
|
||||||
return reducedValue.Valid == false
|
return !reducedValue.Valid
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThresholdEvaluator struct {
|
type ThresholdEvaluator struct {
|
||||||
@@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
|
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
|
||||||
if reducedValue.Valid == false {
|
if !reducedValue.Valid {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
|
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
|
||||||
if reducedValue.Valid == false {
|
if !reducedValue.Valid {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
|||||||
reducedValue := c.Reducer.Reduce(series)
|
reducedValue := c.Reducer.Reduce(series)
|
||||||
evalMatch := c.Evaluator.Eval(reducedValue)
|
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||||
|
|
||||||
if reducedValue.Valid == false {
|
if !reducedValue.Valid {
|
||||||
emptySerieCount++
|
emptySerieCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
|||||||
|
|
||||||
// backward compatibility check, can be removed later
|
// backward compatibility check, can be removed later
|
||||||
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
||||||
if hasEnabled && enabled.MustBool() == false {
|
if hasEnabled && !enabled.MustBool() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
|
|||||||
|
|
||||||
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
|
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||||
var cmd *m.SendWebhookSync
|
var cmd *m.SendWebhookSync
|
||||||
if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
|
if evalContext.ImagePublicUrl == "" && this.UploadImage {
|
||||||
cmd = this.buildMessage(evalContext, true)
|
cmd = this.buildMessage(evalContext, true)
|
||||||
} else {
|
} else {
|
||||||
cmd = this.buildMessage(evalContext, false)
|
cmd = this.buildMessage(evalContext, false)
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ func (e ValidationError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ValueFormatRegex = regexp.MustCompile("^\\d+")
|
ValueFormatRegex = regexp.MustCompile(`^\d+`)
|
||||||
UnitFormatRegex = regexp.MustCompile("\\w{1}$")
|
UnitFormatRegex = regexp.MustCompile(`\w{1}$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var unitMultiplier = map[string]int{
|
var unitMultiplier = map[string]int{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type SchedulerImpl struct {
|
|||||||
|
|
||||||
func NewScheduler() Scheduler {
|
func NewScheduler() Scheduler {
|
||||||
return &SchedulerImpl{
|
return &SchedulerImpl{
|
||||||
jobs: make(map[int64]*Job, 0),
|
jobs: make(map[int64]*Job),
|
||||||
log: log.New("alerting.scheduler"),
|
log: log.New("alerting.scheduler"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ func NewScheduler() Scheduler {
|
|||||||
func (s *SchedulerImpl) Update(rules []*Rule) {
|
func (s *SchedulerImpl) Update(rules []*Rule) {
|
||||||
s.log.Debug("Scheduling update", "ruleCount", len(rules))
|
s.log.Debug("Scheduling update", "ruleCount", len(rules))
|
||||||
|
|
||||||
jobs := make(map[int64]*Job, 0)
|
jobs := make(map[int64]*Job)
|
||||||
|
|
||||||
for i, rule := range rules {
|
for i, rule := range rules {
|
||||||
var job *Job
|
var job *Job
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package notifications
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
@@ -135,7 +134,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
|||||||
subjectText, hasSubject := subjectData["value"]
|
subjectText, hasSubject := subjectData["value"]
|
||||||
|
|
||||||
if !hasSubject {
|
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))
|
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ func Init(ctx context.Context, homePath string, cfg *ini.File) error {
|
|||||||
|
|
||||||
dashboardPath := path.Join(provisioningPath, "dashboards")
|
dashboardPath := path.Join(provisioningPath, "dashboards")
|
||||||
_, err := dashboards.Provision(ctx, dashboardPath)
|
_, err := dashboards.Provision(ctx, dashboardPath)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeAbsolute(path string, root string) string {
|
func makeAbsolute(path string, root string) string {
|
||||||
|
|||||||
@@ -23,12 +23,7 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
|||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
|
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
|
||||||
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
|
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,11 +102,8 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
|||||||
|
|
||||||
existing.Tags = item.Tags
|
existing.Tags = item.Tags
|
||||||
|
|
||||||
if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
|
_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrInvalidApiKey
|
return m.ErrInvalidApiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrInvalidApiKey
|
return m.ErrInvalidApiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not allow plugin dashboard updates without overwrite flag
|
// 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}
|
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ func GetDashboard(query *m.GetDashboardQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrDashboardNotFound
|
return m.ErrDashboardNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
|||||||
has, err := sess.Get(&dashboard)
|
has, err := sess.Get(&dashboard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrDashboardNotFound
|
return m.ErrDashboardNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,12 +347,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
|
|||||||
|
|
||||||
err := x.In("id", query.DashboardIds).Find(&dashboards)
|
err := x.In("id", query.DashboardIds).Find(&dashboards)
|
||||||
query.Result = dashboards
|
query.Result = dashboards
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
|
// 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)
|
err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
|
||||||
query.Result = dashboards
|
query.Result = dashboards
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardSlugDTO struct {
|
type DashboardSlugDTO struct {
|
||||||
@@ -451,7 +441,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if exists == false {
|
} else if !exists {
|
||||||
return m.ErrDashboardNotFound
|
return m.ErrDashboardNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +469,7 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if exists == false {
|
} else if !exists {
|
||||||
return m.ErrDashboardNotFound
|
return m.ErrDashboardNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,7 +559,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not allow plugin dashboard updates without overwrite flag
|
// 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}
|
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,8 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
|
|||||||
|
|
||||||
// Update dashboard HasAcl flag
|
// Update dashboard HasAcl flag
|
||||||
dashboard := m.Dashboard{HasAcl: true}
|
dashboard := m.Dashboard{HasAcl: true}
|
||||||
if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
|
_, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrDashboardSnapshotNotFound
|
return m.ErrDashboardSnapshotNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) {
|
|||||||
addDashboardAclMigrations(mg)
|
addDashboardAclMigrations(mg)
|
||||||
addTagMigration(mg)
|
addTagMigration(mg)
|
||||||
addLoginAttemptMigrations(mg)
|
addLoginAttemptMigrations(mg)
|
||||||
|
addUserAuthMigrations(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMigrationLogMigrations(mg *Migrator) {
|
func addMigrationLogMigrations(mg *Migrator) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -84,8 +84,7 @@ func (db *BaseDialect) DateTimeFunc(value string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDialect) CreateTableSql(table *Table) 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"
|
sql += b.dialect.Quote(table.Name) + " (\n"
|
||||||
|
|
||||||
pkList := table.PrimaryKeys
|
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 {
|
func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
|
||||||
quote := db.dialect.Quote
|
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))
|
return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package migrator
|
package migrator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,7 +112,7 @@ func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
|
|||||||
|
|
||||||
func (m *DropIndexMigration) Sql(dialect Dialect) string {
|
func (m *DropIndexMigration) Sql(dialect Dialect) string {
|
||||||
if m.index.Name == "" {
|
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)
|
return dialect.DropIndexSql(m.tableName, m.index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ type Index struct {
|
|||||||
|
|
||||||
func (index *Index) XName(tableName string) string {
|
func (index *Index) XName(tableName string) string {
|
||||||
if index.Name == "" {
|
if index.Name == "" {
|
||||||
index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_"))
|
index.Name = strings.Join(index.Cols, "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(index.Name, "UQE_") &&
|
if !strings.HasPrefix(index.Name, "UQE_") &&
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
|
|||||||
has, err := x.Get(&pluginSetting)
|
has, err := x.Get(&pluginSetting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrPluginSettingNotFound
|
return m.ErrPluginSettingNotFound
|
||||||
}
|
}
|
||||||
query.Result = &pluginSetting
|
query.Result = &pluginSetting
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
|
|||||||
has, err := x.Get("a)
|
has, err := x.Get("a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
quota.Limit = query.Default
|
quota.Limit = query.Default
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
quota.Limit = cmd.Limit
|
quota.Limit = cmd.Limit
|
||||||
if has == false {
|
if !has {
|
||||||
quota.Created = time.Now()
|
quota.Created = time.Now()
|
||||||
//No quota in the DB for this target, so create a new one.
|
//No quota in the DB for this target, so create a new one.
|
||||||
if _, err := sess.Insert("a); err != nil {
|
if _, err := sess.Insert("a); err != nil {
|
||||||
@@ -133,7 +133,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
|
|||||||
has, err := x.Get("a)
|
has, err := x.Get("a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
quota.Limit = query.Default
|
quota.Limit = query.Default
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
quota.Limit = cmd.Limit
|
quota.Limit = cmd.Limit
|
||||||
if has == false {
|
if !has {
|
||||||
quota.Created = time.Now()
|
quota.Created = time.Now()
|
||||||
//No quota in the DB for this target, so create a new one.
|
//No quota in the DB for this target, so create a new one.
|
||||||
if _, err := sess.Insert("a); err != nil {
|
if _, err := sess.Insert("a); err != nil {
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
|
|||||||
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
|
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
|
||||||
query.Result = make([]*m.DataSourceStats, 0)
|
query.Result = make([]*m.DataSourceStats, 0)
|
||||||
err := x.SQL(rawSql).Find(&query.Result)
|
err := x.SQL(rawSql).Find(&query.Result)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,11 +210,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
|||||||
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
|
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
|
||||||
|
|
||||||
err := sess.Find(&query.Result)
|
err := sess.Find(&query.Result)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTeamMember adds a user to a team
|
// AddTeamMember adds a user to a team
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrTempUserNotFound
|
return m.ErrTempUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func GetUserById(query *m.GetUserByIdQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrUserNotFound
|
return m.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
|
|||||||
return err
|
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
|
// If the user wasn't found, and it contains an "@" fallback to finding the
|
||||||
// user by email.
|
// user by email.
|
||||||
user = &m.User{Email: query.LoginOrEmail}
|
user = &m.User{Email: query.LoginOrEmail}
|
||||||
@@ -188,7 +188,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrUserNotFound
|
return m.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrUserNotFound
|
return m.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,11 +253,8 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
|
|||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
|
_, err := sess.Id(cmd.UserId).Update(&user)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,11 +268,8 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
|
|||||||
LastSeenAt: time.Now(),
|
LastSeenAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
|
_, err := sess.Id(cmd.UserId).Update(&user)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,11 +289,12 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
user := m.User{}
|
user := m.User{
|
||||||
sess.Id(cmd.UserId).Get(&user)
|
Id: cmd.UserId,
|
||||||
|
OrgId: cmd.OrgId,
|
||||||
|
}
|
||||||
|
|
||||||
user.OrgId = cmd.OrgId
|
_, err := sess.Id(cmd.UserId).Update(&user)
|
||||||
_, err := sess.Id(user.Id).Update(&user)
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -310,7 +305,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has == false {
|
} else if !has {
|
||||||
return m.ErrUserNotFound
|
return m.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +440,7 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
|
|||||||
"DELETE FROM dashboard_acl WHERE user_id = ?",
|
"DELETE FROM dashboard_acl WHERE user_id = ?",
|
||||||
"DELETE FROM preferences WHERE user_id = ?",
|
"DELETE FROM preferences WHERE user_id = ?",
|
||||||
"DELETE FROM team_member WHERE user_id = ?",
|
"DELETE FROM team_member WHERE user_id = ?",
|
||||||
|
"DELETE FROM user_auth WHERE user_id = ?",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sql := range deletes {
|
for _, sql := range deletes {
|
||||||
@@ -479,10 +475,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
|
|||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
|
_, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -182,7 +182,7 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
|
|||||||
var data UserInfoJson
|
var data UserInfoJson
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if s.extractToken(&data, token) != true {
|
if !s.extractToken(&data, token) {
|
||||||
response, err := HttpGet(client, s.apiUrl)
|
response, err := HttpGet(client, s.apiUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error getting user info: %s", err)
|
return nil, fmt.Errorf("Error getting user info: %s", err)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
|
|||||||
|
|
||||||
func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
var data struct {
|
var data struct {
|
||||||
|
Id int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Login string `json:"username"`
|
Login string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@@ -69,6 +70,7 @@ func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
userInfo := &BasicUserInfo{
|
userInfo := &BasicUserInfo{
|
||||||
|
Id: fmt.Sprintf("%d", data.Id),
|
||||||
Name: data.Name,
|
Name: data.Name,
|
||||||
Login: data.Login,
|
Login: data.Login,
|
||||||
Email: data.Email,
|
Email: data.Email,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type BasicUserInfo struct {
|
type BasicUserInfo struct {
|
||||||
|
Id string
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
Login string
|
Login string
|
||||||
|
|||||||
@@ -71,15 +71,12 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
|
|||||||
switch queryType {
|
switch queryType {
|
||||||
case "metricFindQuery":
|
case "metricFindQuery":
|
||||||
result, err = e.executeMetricFindQuery(ctx, queryContext)
|
result, err = e.executeMetricFindQuery(ctx, queryContext)
|
||||||
break
|
|
||||||
case "annotationQuery":
|
case "annotationQuery":
|
||||||
result, err = e.executeAnnotationQuery(ctx, queryContext)
|
result, err = e.executeAnnotationQuery(ctx, queryContext)
|
||||||
break
|
|
||||||
case "timeSeriesQuery":
|
case "timeSeriesQuery":
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
result, err = e.executeTimeSeriesQuery(ctx, queryContext)
|
result, err = e.executeTimeSeriesQuery(ctx, queryContext)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
|
|||||||
@@ -175,25 +175,18 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
|
|||||||
switch subType {
|
switch subType {
|
||||||
case "regions":
|
case "regions":
|
||||||
data, err = e.handleGetRegions(ctx, parameters, queryContext)
|
data, err = e.handleGetRegions(ctx, parameters, queryContext)
|
||||||
break
|
|
||||||
case "namespaces":
|
case "namespaces":
|
||||||
data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
|
data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
|
||||||
break
|
|
||||||
case "metrics":
|
case "metrics":
|
||||||
data, err = e.handleGetMetrics(ctx, parameters, queryContext)
|
data, err = e.handleGetMetrics(ctx, parameters, queryContext)
|
||||||
break
|
|
||||||
case "dimension_keys":
|
case "dimension_keys":
|
||||||
data, err = e.handleGetDimensions(ctx, parameters, queryContext)
|
data, err = e.handleGetDimensions(ctx, parameters, queryContext)
|
||||||
break
|
|
||||||
case "dimension_values":
|
case "dimension_values":
|
||||||
data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
|
data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
|
||||||
break
|
|
||||||
case "ebs_volume_ids":
|
case "ebs_volume_ids":
|
||||||
data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
|
data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
|
||||||
break
|
|
||||||
case "ec2_instance_attribute":
|
case "ec2_instance_attribute":
|
||||||
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
|
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transformToTable(data, queryResult)
|
transformToTable(data, queryResult)
|
||||||
@@ -261,7 +254,7 @@ func (e *CloudWatchExecutor) handleGetNamespaces(ctx context.Context, parameters
|
|||||||
keys = append(keys, strings.Split(customNamespaces, ",")...)
|
keys = append(keys, strings.Split(customNamespaces, ",")...)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(sort.StringSlice(keys))
|
sort.Strings(keys)
|
||||||
|
|
||||||
result := make([]suggestData, 0)
|
result := make([]suggestData, 0)
|
||||||
for _, key := range keys {
|
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")
|
return nil, errors.New("Unable to call AWS API")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Sort(sort.StringSlice(namespaceMetrics))
|
sort.Strings(namespaceMetrics)
|
||||||
|
|
||||||
result := make([]suggestData, 0)
|
result := make([]suggestData, 0)
|
||||||
for _, name := range namespaceMetrics {
|
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")
|
return nil, errors.New("Unable to call AWS API")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Sort(sort.StringSlice(dimensionValues))
|
sort.Strings(dimensionValues)
|
||||||
|
|
||||||
result := make([]suggestData, 0)
|
result := make([]suggestData, 0)
|
||||||
for _, name := range dimensionValues {
|
for _, name := range dimensionValues {
|
||||||
@@ -573,11 +566,7 @@ func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
|||||||
}
|
}
|
||||||
return !lastPage
|
return !lastPage
|
||||||
})
|
})
|
||||||
if err != nil {
|
return resp, err
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var metricsCacheLock sync.Mutex
|
var metricsCacheLock sync.Mutex
|
||||||
|
|||||||
@@ -181,10 +181,7 @@ func TestCloudWatchMetrics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMultiSelectValue(t *testing.T) {
|
func TestParseMultiSelectValue(t *testing.T) {
|
||||||
|
values := parseMultiSelectValue(" i-someInstance ")
|
||||||
var values []string
|
|
||||||
|
|
||||||
values = parseMultiSelectValue(" i-someInstance ")
|
|
||||||
assert.Equal(t, []string{"i-someInstance"}, values)
|
assert.Equal(t, []string{"i-someInstance"}, values)
|
||||||
|
|
||||||
values = parseMultiSelectValue("{i-05}")
|
values = parseMultiSelectValue("{i-05}")
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
|
|||||||
// convert types not handled by denisenkom/go-mssqldb
|
// convert types not handled by denisenkom/go-mssqldb
|
||||||
// unhandled types are returned as []byte
|
// unhandled types are returned as []byte
|
||||||
for i := 0; i < len(types); i++ {
|
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() {
|
switch types[i].DatabaseTypeName() {
|
||||||
case "MONEY", "SMALLMONEY", "DECIMAL":
|
case "MONEY", "SMALLMONEY", "DECIMAL":
|
||||||
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
|
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{}
|
fillValue := null.Float{}
|
||||||
if fillMissing {
|
if fillMissing {
|
||||||
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
|
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.Float64 = query.Model.Get("fillValue").MustFloat64()
|
||||||
fillValue.Valid = true
|
fillValue.Valid = true
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if metricIndex >= 0 {
|
if metricIndex >= 0 {
|
||||||
if columnValue, ok := values[metricIndex].(string); ok == true {
|
if columnValue, ok := values[metricIndex].(string); ok {
|
||||||
metric = columnValue
|
metric = columnValue
|
||||||
} else {
|
} 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])
|
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]
|
series, exist := pointsBySeries[metric]
|
||||||
if exist == false {
|
if !exist {
|
||||||
series = &tsdb.TimeSeries{Name: metric}
|
series = &tsdb.TimeSeries{Name: metric}
|
||||||
pointsBySeries[metric] = series
|
pointsBySeries[metric] = series
|
||||||
seriesByQueryOrder.PushBack(metric)
|
seriesByQueryOrder.PushBack(metric)
|
||||||
@@ -279,7 +279,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
|||||||
|
|
||||||
if fillMissing {
|
if fillMissing {
|
||||||
var intervalStart float64
|
var intervalStart float64
|
||||||
if exist == false {
|
if !exist {
|
||||||
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
||||||
} else {
|
} else {
|
||||||
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
|||||||
fillValue := null.Float{}
|
fillValue := null.Float{}
|
||||||
if fillMissing {
|
if fillMissing {
|
||||||
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
|
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.Float64 = query.Model.Get("fillValue").MustFloat64()
|
||||||
fillValue.Valid = true
|
fillValue.Valid = true
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if metricIndex >= 0 {
|
if metricIndex >= 0 {
|
||||||
if columnValue, ok := values[metricIndex].(string); ok == true {
|
if columnValue, ok := values[metricIndex].(string); ok {
|
||||||
metric = columnValue
|
metric = columnValue
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
|
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]
|
series, exist := pointsBySeries[metric]
|
||||||
if exist == false {
|
if !exist {
|
||||||
series = &tsdb.TimeSeries{Name: metric}
|
series = &tsdb.TimeSeries{Name: metric}
|
||||||
pointsBySeries[metric] = series
|
pointsBySeries[metric] = series
|
||||||
seriesByQueryOrder.PushBack(metric)
|
seriesByQueryOrder.PushBack(metric)
|
||||||
@@ -288,7 +288,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
|||||||
|
|
||||||
if fillMissing {
|
if fillMissing {
|
||||||
var intervalStart float64
|
var intervalStart float64
|
||||||
if exist == false {
|
if !exist {
|
||||||
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
||||||
} else {
|
} else {
|
||||||
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
|
|||||||
// convert types not handled by lib/pq
|
// convert types not handled by lib/pq
|
||||||
// unhandled types are returned as []byte
|
// unhandled types are returned as []byte
|
||||||
for i := 0; i < len(types); i++ {
|
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() {
|
switch types[i].DatabaseTypeName() {
|
||||||
case "NUMERIC":
|
case "NUMERIC":
|
||||||
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
|
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{}
|
fillValue := null.Float{}
|
||||||
if fillMissing {
|
if fillMissing {
|
||||||
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
|
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.Float64 = query.Model.Get("fillValue").MustFloat64()
|
||||||
fillValue.Valid = true
|
fillValue.Valid = true
|
||||||
}
|
}
|
||||||
@@ -233,7 +233,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
|
|||||||
}
|
}
|
||||||
|
|
||||||
if metricIndex >= 0 {
|
if metricIndex >= 0 {
|
||||||
if columnValue, ok := values[metricIndex].(string); ok == true {
|
if columnValue, ok := values[metricIndex].(string); ok {
|
||||||
metric = columnValue
|
metric = columnValue
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
|
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]
|
series, exist := pointsBySeries[metric]
|
||||||
if exist == false {
|
if !exist {
|
||||||
series = &tsdb.TimeSeries{Name: metric}
|
series = &tsdb.TimeSeries{Name: metric}
|
||||||
pointsBySeries[metric] = series
|
pointsBySeries[metric] = series
|
||||||
seriesByQueryOrder.PushBack(metric)
|
seriesByQueryOrder.PushBack(metric)
|
||||||
@@ -268,7 +268,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
|
|||||||
|
|
||||||
if fillMissing {
|
if fillMissing {
|
||||||
var intervalStart float64
|
var intervalStart float64
|
||||||
if exist == false {
|
if !exist {
|
||||||
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
||||||
} else {
|
} else {
|
||||||
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
|
|||||||
defer engineCache.Unlock()
|
defer engineCache.Unlock()
|
||||||
|
|
||||||
if engine, present := engineCache.cache[dsInfo.Id]; present {
|
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
|
e.XormEngine = engine
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ func init() {
|
|||||||
|
|
||||||
// IsValidShortUid checks if short unique identifier contains valid characters
|
// IsValidShortUid checks if short unique identifier contains valid characters
|
||||||
func IsValidShortUid(uid string) bool {
|
func IsValidShortUid(uid string) bool {
|
||||||
if !validUidPattern(uid) {
|
return validUidPattern(uid)
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateShortUid generates a short unique identifier.
|
// GenerateShortUid generates a short unique identifier.
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export default class ScrollBar extends React.Component<Props, any> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.scrollbar.update();
|
||||||
|
}
|
||||||
|
|
||||||
handleRef = ref => {
|
handleRef = ref => {
|
||||||
this.container = ref;
|
this.container = ref;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<i class="fa fa-fw fa-sign-in"></i>
|
<i class="fa fa-fw fa-sign-in"></i>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ctrl.loginUrl}}">
|
<a href="{{ctrl.loginUrl}}" target="_self">
|
||||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
<li class="side-menu-header">
|
<li class="side-menu-header">
|
||||||
<span class="sidemenu-item-text">Sign In</span>
|
<span class="sidemenu-item-text">Sign In</span>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import classNames from 'classnames';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { PanelContainer } from './PanelContainer';
|
import { PanelContainer } from './PanelContainer';
|
||||||
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
|
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||||
|
import Highlighter from 'react-highlight-words';
|
||||||
|
|
||||||
export interface AddPanelPanelProps {
|
export interface AddPanelPanelProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -16,21 +17,42 @@ export interface AddPanelPanelProps {
|
|||||||
export interface AddPanelPanelState {
|
export interface AddPanelPanelState {
|
||||||
filter: string;
|
filter: string;
|
||||||
panelPlugins: any[];
|
panelPlugins: any[];
|
||||||
|
copiedPanelPlugins: any[];
|
||||||
|
tab: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelPanelState> {
|
export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelPanelState> {
|
||||||
|
private scrollbar: ScrollBar;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
|
this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
|
||||||
this.renderPanelItem = this.renderPanelItem.bind(this);
|
this.renderPanelItem = this.renderPanelItem.bind(this);
|
||||||
|
this.panelSizeChanged = this.panelSizeChanged.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
panelPlugins: this.getPanelPlugins(),
|
panelPlugins: this.getPanelPlugins(''),
|
||||||
|
copiedPanelPlugins: this.getCopiedPanelPlugins(''),
|
||||||
filter: '',
|
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)
|
let panels = _.chain(config.panels)
|
||||||
.filter({ hideFromList: false })
|
.filter({ hideFromList: false })
|
||||||
.map(item => item)
|
.map(item => item)
|
||||||
@@ -39,6 +61,19 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
|||||||
// add special row type
|
// add special row type
|
||||||
panels.push({ id: 'row', name: 'Row', sort: 8, info: { logos: { small: 'public/img/icn-row.svg' } } });
|
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);
|
let copiedPanelJson = store.get(LS_PANEL_COPY_KEY);
|
||||||
if (copiedPanelJson) {
|
if (copiedPanelJson) {
|
||||||
let copiedPanel = JSON.parse(copiedPanelJson);
|
let copiedPanel = JSON.parse(copiedPanelJson);
|
||||||
@@ -48,12 +83,13 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
|||||||
pluginCopy.name = copiedPanel.title;
|
pluginCopy.name = copiedPanel.title;
|
||||||
pluginCopy.sort = -1;
|
pluginCopy.sort = -1;
|
||||||
pluginCopy.defaults = copiedPanel;
|
pluginCopy.defaults = copiedPanel;
|
||||||
panels.push(pluginCopy);
|
copiedPanels.push(pluginCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add sort by sort property
|
copiedPanels = this.filterPanels(copiedPanels, filter);
|
||||||
return _.sortBy(panels, 'sort');
|
|
||||||
|
return _.sortBy(copiedPanels, 'sort');
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddPanel = panelPluginInfo => {
|
onAddPanel = panelPluginInfo => {
|
||||||
@@ -92,28 +128,117 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
|||||||
dashboard.removePanel(dashboard.panels[0]);
|
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) {
|
renderPanelItem(panel, index) {
|
||||||
return (
|
return (
|
||||||
<div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}>
|
<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} />
|
<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>
|
</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() {
|
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 (
|
return (
|
||||||
<div className="panel-container add-panel-container">
|
<div className="panel-container add-panel-container">
|
||||||
<div className="add-panel">
|
<div className="add-panel">
|
||||||
<div className="add-panel__header">
|
<div className="add-panel__header">
|
||||||
<i className="gicon gicon-add-panel" />
|
<i className="gicon gicon-add-panel" />
|
||||||
<span className="add-panel__title">New Panel</span>
|
<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}>
|
<button className="add-panel__close" onClick={this.handleCloseAddPanel}>
|
||||||
<i className="fa fa-close" />
|
<i className="fa fa-close" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -194,8 +194,8 @@ export class PanelCtrl {
|
|||||||
});
|
});
|
||||||
|
|
||||||
menu.push({
|
menu.push({
|
||||||
text: 'Add to Panel List',
|
text: 'Copy',
|
||||||
click: 'ctrl.addToPanelList()',
|
click: 'ctrl.copyPanel()',
|
||||||
role: 'Editor',
|
role: 'Editor',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -260,9 +260,9 @@ export class PanelCtrl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addToPanelList() {
|
copyPanel() {
|
||||||
store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel()));
|
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) {
|
replacePanel(newPanel, oldPanel) {
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tags/autoComplete/tags',
|
url: '/tags/autoComplete/tags',
|
||||||
params: {
|
params: {
|
||||||
expr: _.map(expressions, expression => templateSrv.replace(expression)),
|
expr: _.map(expressions, expression => templateSrv.replace((expression || '').trim())),
|
||||||
},
|
},
|
||||||
// for cancellations
|
// for cancellations
|
||||||
requestId: options.requestId,
|
requestId: options.requestId,
|
||||||
@@ -355,8 +355,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tags/autoComplete/values',
|
url: '/tags/autoComplete/values',
|
||||||
params: {
|
params: {
|
||||||
expr: _.map(expressions, expression => templateSrv.replace(expression)),
|
expr: _.map(expressions, expression => templateSrv.replace((expression || '').trim())),
|
||||||
tag: templateSrv.replace(tag),
|
tag: templateSrv.replace((tag || '').trim()),
|
||||||
},
|
},
|
||||||
// for cancellations
|
// for cancellations
|
||||||
requestId: options.requestId,
|
requestId: options.requestId,
|
||||||
|
|||||||
@@ -222,4 +222,99 @@ describe('graphiteDatasource', function() {
|
|||||||
expect(results.length).to.be(2);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,9 +11,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-panel__header {
|
.add-panel__header {
|
||||||
padding: 5px 15px;
|
padding: 0 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background: $page-header-bg;
|
||||||
|
box-shadow: $page-header-shadow;
|
||||||
|
border-bottom: 1px solid $page-header-border-color;
|
||||||
|
|
||||||
.gicon {
|
.gicon {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
|
|
||||||
.add-panel__title {
|
.add-panel__title {
|
||||||
font-size: $font-size-md;
|
font-size: $font-size-md;
|
||||||
margin-right: $spacer/2;
|
margin-right: $spacer*2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-panel__sub-title {
|
.add-panel__sub-title {
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -84,3 +88,16 @@
|
|||||||
.add-panel__item-icon {
|
.add-panel__item-icon {
|
||||||
padding: 2px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,18 +44,16 @@
|
|||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: block;
|
display: block;
|
||||||
content: " ";
|
content: ' ';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%);
|
||||||
to right,
|
|
||||||
#ffd500 0%,
|
|
||||||
#ff4400 99%,
|
|
||||||
#ff4400 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.active--panel {
|
||||||
|
background: $panel-bg !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
font-size: $font-size-base * 0.846;
|
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
|
line-height: 14px; // ensure proper line-height if floated
|
||||||
color: $white;
|
color: $white;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
|
|||||||
Reference in New Issue
Block a user