diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c32dcdba40..128abbb1662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ## Minor Enchancements * **Prometheus**: Make Prometheus query field a textarea [#7663](https://github.com/grafana/grafana/issues/7663), thx [@hagen1778](https://github.com/hagen1778) +* **Prometheus**: Step parameter changed semantics to min step to reduce the load on Prometheus and rendering in browser [#8073](https://github.com/grafana/grafana/pull/8073), thx [@bobrik](https://github.com/bobrik) * **Templating**: Should not be possible to create self-referencing (recursive) template variable definitions [#7614](https://github.com/grafana/grafana/issues/7614) thx [@thuck](https://github.com/thuck) * **Cloudwatch**: Correctly obtain IAM roles within ECS container tasks [#7892](https://github.com/grafana/grafana/issues/7892) thx [@gomlgs](https://github.com/gomlgs) * **Units**: New number format: Scientific notation [#7781](https://github.com/grafana/grafana/issues/7781) thx [@cadnce](https://github.com/cadnce) diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index cc7ce4c2650..22aadb80f58 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -12,8 +12,8 @@ parent = "http_api" # Alerting API -You can use the Alerting API to get information about alerts and their states but this API cannot be used to modify the alert. -To create new alerts or modify them you need to update the dashboard json that contains the alerts. +You can use the Alerting API to get information about alerts and their states but this API cannot be used to modify the alert. +To create new alerts or modify them you need to update the dashboard json that contains the alerts. This API can also be used to create, update and delete alert notifications. @@ -115,7 +115,7 @@ This API can also be used to create, update and delete alert notifications. HTTP/1.1 200 Content-Type: application/json - + { "id": 1, "name": "Team A", @@ -127,11 +127,11 @@ This API can also be used to create, update and delete alert notifications. ## Create alert notification -`POST /api/alerts-notifications` +`POST /api/alert-notifications` **Example Request**: - POST /api/alerts-notifications HTTP/1.1 + POST /api/alert-notifications HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -144,29 +144,29 @@ This API can also be used to create, update and delete alert notifications. "addresses": "carl@grafana.com;dev@grafana.com" } } - + **Example Response**: HTTP/1.1 200 Content-Type: application/json { - "id": 1, + "id": 1, "name": "new alert notification", "type": "email", "isDefault": false, "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } - "created": "2017-01-01 12:34", + "created": "2017-01-01 12:34", "updated": "2017-01-01 12:34" } ## Update alert notification -`PUT /api/alerts-notifications/1` +`PUT /api/alert-notifications/1` **Example Request**: - PUT /api/alerts-notifications/1 HTTP/1.1 + PUT /api/alert-notifications/1 HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -176,29 +176,29 @@ This API can also be used to create, update and delete alert notifications. "name": "new alert notification", //Required "type": "email", //Required "isDefault": false, - "settings": { + "settings": { "addresses: "carl@grafana.com;dev@grafana.com" } } - + **Example Response**: HTTP/1.1 200 Content-Type: application/json { - "id": 1, + "id": 1, "name": "new alert notification", "type": "email", "isDefault": false, "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } - "created": "2017-01-01 12:34", + "created": "2017-01-01 12:34", "updated": "2017-01-01 12:34" } ## Delete alert notification -`DELETE /api/alerts-notifications/:notificationId` +`DELETE /api/alert-notifications/:notificationId` **Example Request**: diff --git a/pkg/log/log.go b/pkg/log/log.go index fe0b312db23..f69d0f6b9b4 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -34,7 +34,7 @@ func New(logger string, ctx ...interface{}) Logger { func Trace(format string, v ...interface{}) { var message string if len(v) > 0 { - message = fmt.Sprintf(format, v) + message = fmt.Sprintf(format, v...) } else { message = format } @@ -45,7 +45,7 @@ func Trace(format string, v ...interface{}) { func Debug(format string, v ...interface{}) { var message string if len(v) > 0 { - message = fmt.Sprintf(format, v) + message = fmt.Sprintf(format, v...) } else { message = format } @@ -60,7 +60,7 @@ func Debug2(message string, v ...interface{}) { func Info(format string, v ...interface{}) { var message string if len(v) > 0 { - message = fmt.Sprintf(format, v) + message = fmt.Sprintf(format, v...) } else { message = format } @@ -75,7 +75,7 @@ func Info2(message string, v ...interface{}) { func Warn(format string, v ...interface{}) { var message string if len(v) > 0 { - message = fmt.Sprintf(format, v) + message = fmt.Sprintf(format, v...) } else { message = format } @@ -88,7 +88,7 @@ func Warn2(message string, v ...interface{}) { } func Error(skip int, format string, v ...interface{}) { - Root.Error(fmt.Sprintf(format, v)) + Root.Error(fmt.Sprintf(format, v...)) } func Error2(message string, v ...interface{}) { @@ -96,7 +96,7 @@ func Error2(message string, v ...interface{}) { } func Critical(skip int, format string, v ...interface{}) { - Root.Crit(fmt.Sprintf(format, v)) + Root.Crit(fmt.Sprintf(format, v...)) } func Fatal(skip int, format string, v ...interface{}) { diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index e02e31f9152..8e94e1582b0 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func initContextWithAuthProxy(ctx *Context) bool { +func initContextWithAuthProxy(ctx *Context, orgId int64) bool { if !setting.AuthProxyEnabled { return false } @@ -30,6 +30,7 @@ func initContextWithAuthProxy(ctx *Context) bool { } 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) @@ -46,7 +47,7 @@ func initContextWithAuthProxy(ctx *Context) bool { ctx.Handle(500, "Failed to create user specified in auth proxy header", err) return true } - query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id} + 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 diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 4b59fada62e..5aafe12d374 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -39,6 +39,12 @@ func GetContextHandler() macaron.Handler { Logger: log.New("context"), } + orgId := int64(0) + orgIdHeader := ctx.Req.Header.Get("X-Grafana-Org-Id") + if orgIdHeader != "" { + orgId, _ = strconv.ParseInt(orgIdHeader, 10, 64) + } + // the order in which these are tested are important // look for api key in Authorization header first // then init session and look for userId in session @@ -46,9 +52,9 @@ func GetContextHandler() macaron.Handler { // then test if anonymous access is enabled if initContextWithRenderAuth(ctx) || initContextWithApiKey(ctx) || - initContextWithBasicAuth(ctx) || - initContextWithAuthProxy(ctx) || - initContextWithUserSessionCookie(ctx) || + initContextWithBasicAuth(ctx, orgId) || + initContextWithAuthProxy(ctx, orgId) || + initContextWithUserSessionCookie(ctx, orgId) || initContextWithAnonymousUser(ctx) { } @@ -68,18 +74,18 @@ func initContextWithAnonymousUser(ctx *Context) bool { if err := bus.Dispatch(&orgQuery); err != nil { log.Error(3, "Anonymous access organization error: '%s': %s", setting.AnonymousOrgName, err) return false - } else { - ctx.IsSignedIn = false - ctx.AllowAnonymous = true - ctx.SignedInUser = &m.SignedInUser{} - ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole) - ctx.OrgId = orgQuery.Result.Id - ctx.OrgName = orgQuery.Result.Name - return true } + + ctx.IsSignedIn = false + ctx.AllowAnonymous = true + ctx.SignedInUser = &m.SignedInUser{} + ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole) + ctx.OrgId = orgQuery.Result.Id + ctx.OrgName = orgQuery.Result.Name + return true } -func initContextWithUserSessionCookie(ctx *Context) bool { +func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool { // initialize session if err := ctx.Session.Start(ctx); err != nil { ctx.Logger.Error("Failed to start session", "error", err) @@ -91,15 +97,15 @@ func initContextWithUserSessionCookie(ctx *Context) bool { return false } - query := m.GetSignedInUserQuery{UserId: userId} + query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId} if err := bus.Dispatch(&query); err != nil { ctx.Logger.Error("Failed to get user with id", "userId", userId) return false - } else { - ctx.SignedInUser = query.Result - ctx.IsSignedIn = true - return true } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + return true } func initContextWithApiKey(ctx *Context) bool { @@ -114,30 +120,31 @@ func initContextWithApiKey(ctx *Context) bool { ctx.JsonApiErr(401, "Invalid API key", err) return true } + // fetch key keyQuery := m.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId} if err := bus.Dispatch(&keyQuery); err != nil { ctx.JsonApiErr(401, "Invalid API key", err) return true - } else { - apikey := keyQuery.Result + } - // validate api key - if !apikeygen.IsValid(decoded, apikey.Key) { - ctx.JsonApiErr(401, "Invalid API key", err) - return true - } + apikey := keyQuery.Result - ctx.IsSignedIn = true - ctx.SignedInUser = &m.SignedInUser{} - ctx.OrgRole = apikey.Role - ctx.ApiKeyId = apikey.Id - ctx.OrgId = apikey.OrgId + // validate api key + if !apikeygen.IsValid(decoded, apikey.Key) { + ctx.JsonApiErr(401, "Invalid API key", err) return true } + + ctx.IsSignedIn = true + ctx.SignedInUser = &m.SignedInUser{} + ctx.OrgRole = apikey.Role + ctx.ApiKeyId = apikey.Id + ctx.OrgId = apikey.OrgId + return true } -func initContextWithBasicAuth(ctx *Context) bool { +func initContextWithBasicAuth(ctx *Context, orgId int64) bool { if !setting.BasicAuthEnabled { return false @@ -168,15 +175,15 @@ func initContextWithBasicAuth(ctx *Context) bool { return true } - query := m.GetSignedInUserQuery{UserId: user.Id} + query := m.GetSignedInUserQuery{UserId: user.Id, OrgId: orgId} if err := bus.Dispatch(&query); err != nil { ctx.JsonApiErr(401, "Authentication error", err) return true - } else { - ctx.SignedInUser = query.Result - ctx.IsSignedIn = true - return true } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + return true } // Handle handles and logs error by given status. diff --git a/pkg/models/user.go b/pkg/models/user.go index e0a36be8c0a..bdf81056232 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -117,6 +117,7 @@ type GetSignedInUserQuery struct { UserId int64 Login string Email string + OrgId int64 Result *SignedInUser } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 9a44d6f194e..71b29bd3355 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -1,6 +1,7 @@ package sqlstore import ( + "strconv" "strings" "time" @@ -273,7 +274,7 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error { } if !valid { - return fmt.Errorf("user does not belong ot org") + return fmt.Errorf("user does not belong to org") } return inTransaction(func(sess *xorm.Session) error { @@ -319,19 +320,24 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error { } func GetSignedInUser(query *m.GetSignedInUserQuery) error { + orgId := "u.org_id" + if query.OrgId > 0 { + orgId = strconv.FormatInt(query.OrgId, 10) + } + var rawSql = `SELECT - u.id as user_id, - u.is_admin as is_grafana_admin, - u.email as email, - u.login as login, - u.name as name, - u.help_flags1 as help_flags1, - org.name as org_name, - org_user.role as org_role, - org.id as org_id - FROM ` + dialect.Quote("user") + ` as u - LEFT OUTER JOIN org_user on org_user.org_id = u.org_id and org_user.user_id = u.id - LEFT OUTER JOIN org on org.id = u.org_id ` + u.id as user_id, + u.is_admin as is_grafana_admin, + u.email as email, + u.login as login, + u.name as name, + u.help_flags1 as help_flags1, + org.name as org_name, + org_user.role as org_role, + org.id as org_id + FROM ` + dialect.Quote("user") + ` as u + LEFT OUTER JOIN org_user on org_user.org_id = ` + orgId + ` and org_user.user_id = u.id + LEFT OUTER JOIN org on org.id = org_user.org_id ` sess := x.Table("user") if query.UserId > 0 { diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 6b2a300551e..df90728157d 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -40,7 +40,14 @@ export function infoPopover() { openOn: openOn, hoverOpenDelay: 400, tetherOptions: { - offset: offset + offset: offset, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ], } }); diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index ba4f4dc2fb9..041cd1ab1db 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -9,8 +9,8 @@ export class BackendSrv { inFlightRequests = {}; HTTP_REQUEST_CANCELLED = -1; - /** @ngInject */ - constructor(private $http, private alertSrv, private $rootScope, private $q, private $timeout) { + /** @ngInject */ + constructor(private $http, private alertSrv, private $rootScope, private $q, private $timeout, private contextSrv) { } get(url, params?) { @@ -63,12 +63,18 @@ export class BackendSrv { request(options) { options.retry = options.retry || 0; - var requestIsLocal = options.url.indexOf('/') === 0; + var requestIsLocal = !options.url.match(/^http/); var firstAttempt = options.retry === 0; - if (requestIsLocal && !options.hasSubUrl) { - options.url = config.appSubUrl + options.url; - options.hasSubUrl = true; + if (requestIsLocal) { + if (this.contextSrv.user && this.contextSrv.user.orgId) { + options.headers = options.headers || {}; + options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; + } + + if (options.url.indexOf("/") === 0) { + options.url = options.url.substring(1); + } } return this.$http(options).then(results => { @@ -125,16 +131,23 @@ export class BackendSrv { this.addCanceler(requestId, canceler); } - var requestIsLocal = options.url.indexOf('/') === 0; + var requestIsLocal = !options.url.match(/^http/); var firstAttempt = options.retry === 0; - if (requestIsLocal && !options.hasSubUrl && options.retry === 0) { - options.url = config.appSubUrl + options.url; - } + if (requestIsLocal) { + if (this.contextSrv.user && this.contextSrv.user.orgId) { + options.headers = options.headers || {}; + options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; + } - if (requestIsLocal && options.headers && options.headers.Authorization) { - options.headers['X-DS-Authorization'] = options.headers.Authorization; - delete options.headers.Authorization; + if (options.url.indexOf("/") === 0) { + options.url = options.url.substring(1); + } + + if (options.headers && options.headers.Authorization) { + options.headers['X-DS-Authorization'] = options.headers.Authorization; + delete options.headers.Authorization; + } } return this.$http(options).catch(err => { diff --git a/public/app/plugins/datasource/influxdb/partials/query.options.html b/public/app/plugins/datasource/influxdb/partials/query.options.html index 2dd6b4d376b..80e7f0e59ea 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.options.html +++ b/public/app/plugins/datasource/influxdb/partials/query.options.html @@ -54,7 +54,7 @@