diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5ecbc8397df..22642808fa4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,7 @@ Fixes # **Release note**: ```release-note diff --git a/.prettierignore b/.prettierignore index b7a33870ddd..336d03e2551 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,5 @@ pkg/ node_modules public/vendor/ vendor/ +data/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 677a12831dc..d21ef5232d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,10 +34,10 @@ To setup a local development environment we recommend reading [Building Grafana ### Pull requests with new features Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests). -Make sure to include `closes #` or `fixes #` in the pull request description. +Make sure to include `Closes #` or `Fixes #` in the pull request description. ### Pull requests with bug fixes -Please make all changes in one commit if possible. Include `closes #12345` in bottom of the commit message. +Please make all changes in one commit if possible. Include `Closes #` in bottom of the commit message. A commit message for a bug fix should look something like this. ``` @@ -48,7 +48,7 @@ provsioners each provisioner overwrite each other. filling up dashboard_versions quite fast if using default settings. -closes #12864 +Closes #12864 ``` -If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged. \ No newline at end of file +If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged. diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index 1a83432b8f7..510776750f3 100644 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -217,10 +217,10 @@ Some OAuth2 providers might not support `client_id` and `client_secret` passed v results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be send via POST body, which can be enabled via the following settings: - ```bash - [auth.generic_oauth] - send_client_credentials_via_post = true - ``` +```bash +[auth.generic_oauth] +send_client_credentials_via_post = true +```
diff --git a/package.json b/package.json index a937ba6f717..d2760bbad02 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build", "gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release", "gui:publish": "cd packages/grafana-ui/dist && npm publish --access public", - "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p", + "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p --createVersionCommit", "cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts" }, "husky": { diff --git a/packages/grafana-ui/README.md b/packages/grafana-ui/README.md index fa482003253..935124e99ba 100644 --- a/packages/grafana-ui/README.md +++ b/packages/grafana-ui/README.md @@ -12,6 +12,36 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra `npm install @grafana/ui` +## Development + +For development purposes we suggest using `yarn link` that will create symlink to @grafana/ui lib. To do so navigate to `packages/grafana-ui` and run `yarn link`. Then, navigate to your project and run `yarn link @grafana/ui` to use the linked version of the lib. To unlink follow the same procedure, but use `yarn unlink` instead. + +## Building @grafana/ui +To build @grafana/ui run `npm run gui:build` script *from Grafana repository root*. The build will be created in `packages/grafana-ui/dist` directory. Following steps from [Development](#development) you can test built package. + +## Releasing new version +To release new version run `npm run gui:release` script *from Grafana repository root*. The script will prepare the distribution package as well as prompt you to bump library version and publish it to the NPM registry. + +### Automatic version bump +When running `npm run gui:release` package.json file will be automatically updated. Also, package.json file will be commited and pushed to upstream branch. + +### Manual version bump +To use `package.json` defined version run `npm run gui:release --usePackageJsonVersion` *from Grafana repository root*. + +### Preparing release package without publishing to NPM registry +For testing purposes there is `npm run gui:releasePrepare` task that prepares distribution package without publishing it to the NPM registry. + +### V1 release process overview +1. Package is compiled with TSC. Typings are created in `/dist` directory, and the compiled js lands in `/compiled` dir +2. Rollup creates a CommonJS package based on compiled sources, and outputs it to `/dist` directory +3. Readme, changelog and index.js files are moved to `/dist` directory +4. Package version is bumped in both `@grafana/ui` package dir and in dist directory. +5. Version commit is created and pushed to master branch +5. Package is published to npm + + ## Versioning To limit the confusion related to @grafana/ui and Grafana versioning we decided to keep the major version in sync between those two. This means, that first version of @grafana/ui is taged with 6.0.0-alpha.0 to keep version in sync with Grafana 6.0 release. + + diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 8bb331b7e59..24ab6fdc0f8 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -219,8 +219,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo } func (a *ldapAuther) serverBind() error { + bindFn := func() error { + return a.conn.Bind(a.server.BindDN, a.server.BindPassword) + } + + if a.server.BindPassword == "" { + bindFn = func() error { + return a.conn.UnauthenticatedBind(a.server.BindDN) + } + } + // bind_dn and bind_password to bind - if err := a.conn.Bind(a.server.BindDN, a.server.BindPassword); err != nil { + if err := bindFn(); err != nil { a.log.Info("LDAP initial bind failed, %v", err) if ldapErr, ok := err.(*ldap.Error); ok { diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index dabafee65a6..543cc90378c 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -78,6 +78,69 @@ func TestLdapAuther(t *testing.T) { }) }) + Convey("serverBind", t, func() { + Convey("Given bind dn and password configured", func() { + conn := &mockLdapConn{} + var actualUsername, actualPassword string + conn.bindProvider = func(username, password string) error { + actualUsername = username + actualPassword = password + return nil + } + ldapAuther := &ldapAuther{ + conn: conn, + server: &LdapServerConf{ + BindDN: "o=users,dc=grafana,dc=org", + BindPassword: "bindpwd", + }, + } + err := ldapAuther.serverBind() + So(err, ShouldBeNil) + So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org") + So(actualPassword, ShouldEqual, "bindpwd") + }) + + Convey("Given bind dn configured", func() { + conn := &mockLdapConn{} + unauthenticatedBindWasCalled := false + var actualUsername string + conn.unauthenticatedBindProvider = func(username string) error { + unauthenticatedBindWasCalled = true + actualUsername = username + return nil + } + ldapAuther := &ldapAuther{ + conn: conn, + server: &LdapServerConf{ + BindDN: "o=users,dc=grafana,dc=org", + }, + } + err := ldapAuther.serverBind() + So(err, ShouldBeNil) + So(unauthenticatedBindWasCalled, ShouldBeTrue) + So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org") + }) + + Convey("Given empty bind dn and password", func() { + conn := &mockLdapConn{} + unauthenticatedBindWasCalled := false + var actualUsername string + conn.unauthenticatedBindProvider = func(username string) error { + unauthenticatedBindWasCalled = true + actualUsername = username + return nil + } + ldapAuther := &ldapAuther{ + conn: conn, + server: &LdapServerConf{}, + } + err := ldapAuther.serverBind() + So(err, ShouldBeNil) + So(unauthenticatedBindWasCalled, ShouldBeTrue) + So(actualUsername, ShouldBeEmpty) + }) + }) + Convey("When translating ldap user to grafana user", t, func() { var user1 = &m.User{} diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 1ef085c82f1..3e3496622b7 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -1,6 +1,10 @@ package notifiers import ( + "fmt" + "net/url" + "strings" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" @@ -8,19 +12,26 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" ) -func init() { - alerting.RegisterNotifier(&alerting.NotifierPlugin{ - Type: "dingding", - Name: "DingDing", - Description: "Sends HTTP POST request to DingDing", - Factory: NewDingDingNotifier, - OptionsTemplate: ` +const DefaultDingdingMsgType = "link" +const DingdingOptionsTemplate = `

DingDing settings

Url - +
- `, +
+ MessageType + +
+` + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "dingding", + Name: "DingDing", + Description: "Sends HTTP POST request to DingDing", + Factory: NewDingDingNotifier, + OptionsTemplate: DingdingOptionsTemplate, }) } @@ -31,8 +42,11 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } + msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType) + return &DingDingNotifier{ NotifierBase: NewNotifierBase(model), + MsgType: msgType, Url: url, log: log.New("alerting.notifier.dingding"), }, nil @@ -40,8 +54,9 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) type DingDingNotifier struct { NotifierBase - Url string - log log.Logger + MsgType string + Url string + log log.Logger } func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { @@ -52,6 +67,16 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name) messageUrl = "" } + + q := url.Values{ + "pc_slide": {"false"}, + "url": {messageUrl}, + } + + // Use special link to auto open the message url outside of Dingding + // Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9 + messageUrl = "dingtalk://dingtalkclient/page/link?" + q.Encode() + this.log.Info("messageUrl:" + messageUrl) message := evalContext.Rule.Message @@ -61,15 +86,39 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { message = title } - bodyJSON, err := simplejson.NewJson([]byte(`{ - "msgtype": "link", - "link": { - "text": "` + message + `", - "title": "` + title + `", - "picUrl": "` + picUrl + `", - "messageUrl": "` + messageUrl + `" + for i, match := range evalContext.EvalMatches { + message += fmt.Sprintf("\\n%2d. %s: %s", i+1, match.Metric, match.Value) + } + + var bodyStr string + if this.MsgType == "actionCard" { + // Embed the pic into the markdown directly because actionCard doesn't have a picUrl field + if picUrl != "" { + message = "![](" + picUrl + ")\\n\\n" + message } - }`)) + + bodyStr = `{ + "msgtype": "actionCard", + "actionCard": { + "text": "` + strings.Replace(message, `"`, "'", -1) + `", + "title": "` + strings.Replace(title, `"`, "'", -1) + `", + "singleTitle": "More", + "singleURL": "` + messageUrl + `" + } + }` + } else { + bodyStr = `{ + "msgtype": "link", + "link": { + "text": "` + message + `", + "title": "` + title + `", + "picUrl": "` + picUrl + `", + "messageUrl": "` + messageUrl + `" + } + }` + } + + bodyJSON, err := simplejson.NewJson([]byte(bodyStr)) if err != nil { this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name) diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go index ceff2394dce..fe77dd7e914 100644 --- a/pkg/services/sqlstore/login_attempt.go +++ b/pkg/services/sqlstore/login_attempt.go @@ -44,6 +44,10 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error { return err } + if result == nil || len(result) == 0 || result[0] == nil { + return nil + } + maxId = toInt64(result[0]["id"]) if maxId == 0 { diff --git a/public/app/core/specs/kbn.test.ts b/public/app/core/specs/kbn.test.ts new file mode 100644 index 00000000000..c97e2e1101a --- /dev/null +++ b/public/app/core/specs/kbn.test.ts @@ -0,0 +1,15 @@ +import kbn from '../utils/kbn'; + +describe('stringToJsRegex', () => { + it('should parse the valid regex value', () => { + const output = kbn.stringToJsRegex('/validRegexp/'); + expect(output).toBeInstanceOf(RegExp); + }); + + it('should throw error on invalid regex value', () => { + const input = '/etc/hostname'; + expect(() => { + kbn.stringToJsRegex(input); + }).toThrow(); + }); +}); diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 887c30229d3..43886fafd07 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -234,6 +234,11 @@ kbn.stringToJsRegex = str => { } const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); + + if (!match) { + throw new Error(`'${str}' is not a valid regular expression.`); + } + return new RegExp(match[1], match[2]); }; diff --git a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts index 60fa031f71c..23ad97708b4 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts @@ -16,18 +16,20 @@ const template = `