Merge branch 'master' into alerting_reminder

* master: (95 commits)
  registry: adds more comments
  registry: adds comments to interfaces
  changelog: update
  changelog: update
  changelog: add notes about closing #12438
  alerting: only log when screenshot been uploaded
  fixes typos
  changelog: add notes about closing #12444
  Revert "auth proxy: use real ip when validating white listed ip's"
  changelog: adds note for #11892
  changelog: add notes about closing #12430
  fix footer css issue
  Karma to Jest: 3 test files (#12414)
  fix: log close/flush was done too early, before server shutdown log message was called, fixes #12438
  Karma to Jest: value_select_dropdown (#12435)
  support passing api token in Basic auth password (#12416)
  Add disabled styles for checked checkbox (#12422)
  changelog: add notes about closing #11920
  changelog: add notes about closing #11920
  changelog: update
  ...
This commit is contained in:
bergquist
2018-06-29 15:44:20 +02:00
116 changed files with 5832 additions and 2605 deletions
+9 -6
View File
@@ -37,7 +37,6 @@ func GetAnnotations(c *m.ReqContext) Response {
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
item.Time = item.Time
}
return JSON(200, items)
@@ -214,7 +213,9 @@ func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response
repo := annotations.GetRepository()
err := repo.Delete(&annotations.DeleteParams{
AlertId: cmd.PanelId,
OrgId: c.OrgId,
Id: cmd.AnnotationId,
RegionId: cmd.RegionId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
})
@@ -235,7 +236,8 @@ func DeleteAnnotationByID(c *m.ReqContext) Response {
}
err := repo.Delete(&annotations.DeleteParams{
Id: annotationID,
OrgId: c.OrgId,
Id: annotationID,
})
if err != nil {
@@ -254,6 +256,7 @@ func DeleteAnnotationRegion(c *m.ReqContext) Response {
}
err := repo.Delete(&annotations.DeleteParams{
OrgId: c.OrgId,
RegionId: regionID,
})
@@ -269,9 +272,9 @@ func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
return false, nil
}
if dashboardID > 0 {
guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
if dashboardID != 0 {
guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err
}
}
+47
View File
@@ -100,6 +100,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
Id: 1,
}
deleteCmd := dtos.DeleteAnnotationsCmd{
DashboardId: 1,
PanelId: 1,
}
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
@@ -171,6 +176,25 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
})
})
})
Convey("When user is an Admin", func() {
role := m.ROLE_ADMIN
Convey("Should be able to do anything", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
}
@@ -239,3 +263,26 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m.
fn(sc)
})
}
func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return DeleteAnnotations(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
+27 -26
View File
@@ -4,6 +4,7 @@ import (
"github.com/go-macaron/binding"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
@@ -117,10 +118,10 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/api/login/ping", quota("session"), LoginAPIPing)
// authed api
r.Group("/api", func(apiRoute RouteRegister) {
r.Group("/api", func(apiRoute routing.RouteRegister) {
// user (signed in)
apiRoute.Group("/user", func(userRoute RouteRegister) {
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
userRoute.Get("/", wrap(GetSignedInUser))
userRoute.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
userRoute.Post("/using/:id", wrap(UserSetUsingOrg))
@@ -140,7 +141,7 @@ func (hs *HTTPServer) registerRoutes() {
})
// users (admin permission required)
apiRoute.Group("/users", func(usersRoute RouteRegister) {
apiRoute.Group("/users", func(usersRoute routing.RouteRegister) {
usersRoute.Get("/", wrap(SearchUsers))
usersRoute.Get("/search", wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", wrap(GetUserByID))
@@ -152,7 +153,7 @@ func (hs *HTTPServer) registerRoutes() {
}, reqGrafanaAdmin)
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
@@ -162,19 +163,19 @@ func (hs *HTTPServer) registerRoutes() {
}, reqOrgAdmin)
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
teamsRoute.Get("/search", wrap(SearchTeams))
})
// org information available to all users.
apiRoute.Group("/org", func(orgRoute RouteRegister) {
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent))
orgRoute.Get("/quotas", wrap(GetOrgQuotas))
})
// current org
apiRoute.Group("/org", func(orgRoute RouteRegister) {
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
@@ -192,7 +193,7 @@ func (hs *HTTPServer) registerRoutes() {
}, reqOrgAdmin)
// current org without requirement of user to be org admin
apiRoute.Group("/org", func(orgRoute RouteRegister) {
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
})
@@ -203,7 +204,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
// orgs (admin routes)
apiRoute.Group("/orgs/:orgId", func(orgsRoute RouteRegister) {
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
orgsRoute.Get("/", wrap(GetOrgByID))
orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg))
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress))
@@ -217,24 +218,24 @@ func (hs *HTTPServer) registerRoutes() {
}, reqGrafanaAdmin)
// orgs (admin routes)
apiRoute.Group("/orgs/name/:name", func(orgsRoute RouteRegister) {
apiRoute.Group("/orgs/name/:name", func(orgsRoute routing.RouteRegister) {
orgsRoute.Get("/", wrap(GetOrgByName))
}, reqGrafanaAdmin)
// auth api keys
apiRoute.Group("/auth/keys", func(keysRoute RouteRegister) {
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
keysRoute.Get("/", wrap(GetAPIKeys))
keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey))
keysRoute.Delete("/:id", wrap(DeleteAPIKey))
}, reqOrgAdmin)
// Preferences
apiRoute.Group("/preferences", func(prefRoute RouteRegister) {
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
})
// Data sources
apiRoute.Group("/datasources", func(datasourceRoute RouteRegister) {
apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
datasourceRoute.Get("/", wrap(GetDataSources))
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource))
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
@@ -250,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID))
apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
apiRoute.Group("/plugins", func(pluginRoute RouteRegister) {
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
pluginRoute.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
}, reqOrgAdmin)
@@ -260,17 +261,17 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
// Folders
apiRoute.Group("/folders", func(folderRoute RouteRegister) {
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
folderRoute.Get("/", wrap(GetFolders))
folderRoute.Get("/id/:id", wrap(GetFolderByID))
folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) {
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", wrap(GetFolderByUID))
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
folderUidRoute.Delete("/", wrap(DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute RouteRegister) {
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", wrap(GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions))
})
@@ -278,7 +279,7 @@ func (hs *HTTPServer) registerRoutes() {
})
// Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID))
@@ -292,12 +293,12 @@ func (hs *HTTPServer) registerRoutes() {
dashboardRoute.Get("/tags", GetDashboardTags)
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute RouteRegister) {
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions))
})
@@ -305,12 +306,12 @@ func (hs *HTTPServer) registerRoutes() {
})
// Dashboard snapshots
apiRoute.Group("/dashboard/snapshots", func(dashboardRoute RouteRegister) {
apiRoute.Group("/dashboard/snapshots", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/", wrap(SearchDashboardSnapshots))
})
// Playlist
apiRoute.Group("/playlists", func(playlistRoute RouteRegister) {
apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
playlistRoute.Get("/", wrap(SearchPlaylists))
playlistRoute.Get("/:id", ValidateOrgPlaylist, wrap(GetPlaylist))
playlistRoute.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems))
@@ -329,7 +330,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData))
apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk))
apiRoute.Group("/alerts", func(alertsRoute RouteRegister) {
apiRoute.Group("/alerts", func(alertsRoute routing.RouteRegister) {
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
@@ -340,7 +341,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/alert-notifications", wrap(GetAlertNotifications))
apiRoute.Get("/alert-notifiers", wrap(GetAlertNotifiers))
apiRoute.Group("/alert-notifications", func(alertNotifications RouteRegister) {
apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
@@ -351,7 +352,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/annotations", wrap(GetAnnotations))
apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
@@ -365,7 +366,7 @@ func (hs *HTTPServer) registerRoutes() {
}, reqSignedIn)
// admin api
r.Group("/api/admin", func(adminRoute RouteRegister) {
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
adminRoute.Get("/settings", AdminGetSettings)
adminRoute.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
adminRoute.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
+3
View File
@@ -103,6 +103,9 @@ func DeleteDataSourceByName(c *m.ReqContext) Response {
getCmd := &m.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
if err := bus.Dispatch(getCmd); err != nil {
if err == m.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to delete datasource", err)
}
+8
View File
@@ -46,5 +46,13 @@ func TestDataSourcesProxy(t *testing.T) {
So(respJSON[3]["name"], ShouldEqual, "ZZZ")
})
})
Convey("Should be able to save a data source", func() {
loggedInUserScenario("When calling DELETE on non-existing", "/api/datasources/name/12345", func(sc *scenarioContext) {
sc.handlerFunc = DeleteDataSourceByName
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 404)
})
})
})
}
+5 -4
View File
@@ -11,6 +11,7 @@ import (
"path"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -43,10 +44,10 @@ type HTTPServer struct {
cache *gocache.Cache
httpSrv *http.Server
RouteRegister RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
}
func (hs *HTTPServer) Init() error {
+27
View File
@@ -117,6 +117,28 @@ func (proxy *DataSourceProxy) addTraceFromHeaderValue(span opentracing.Span, hea
}
}
func (proxy *DataSourceProxy) useCustomHeaders(req *http.Request) {
decryptSdj := proxy.ds.SecureJsonData.Decrypt()
index := 1
for {
headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index)
headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index)
if key := proxy.ds.JsonData.Get(headerNameSuffix).MustString(); key != "" {
if val, ok := decryptSdj[headerValueSuffix]; ok {
// remove if exists
if req.Header.Get(key) != "" {
req.Header.Del(key)
}
req.Header.Add(key, val)
logger.Debug("Using custom header ", "CustomHeaders", key)
}
} else {
break
}
index += 1
}
}
func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
return func(req *http.Request) {
req.URL.Scheme = proxy.targetUrl.Scheme
@@ -146,6 +168,11 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.BasicAuthPassword))
}
// Lookup and use custom headers
if proxy.ds.SecureJsonData != nil {
proxy.useCustomHeaders(req)
}
dsAuth := req.Header.Get("X-DS-Authorization")
if len(dsAuth) > 0 {
req.Header.Del("X-DS-Authorization")
+32
View File
@@ -12,6 +12,7 @@ import (
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
@@ -322,6 +323,37 @@ func TestDSRouteRule(t *testing.T) {
So(interpolated, ShouldEqual, "0asd+asd")
})
Convey("When proxying a data source with custom headers specified", func() {
plugin := &plugins.DataSourcePlugin{}
encryptedData, err := util.Encrypt([]byte(`Bearer xf5yhfkpsnmgo`), setting.SecretKey)
ds := &m.DataSource{
Type: m.DS_PROMETHEUS,
Url: "http://prometheus:9090",
JsonData: simplejson.NewFromAny(map[string]interface{}{
"httpHeaderName1": "Authorization",
}),
SecureJsonData: map[string][]byte{
"httpHeaderValue1": encryptedData,
},
}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestURL, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestURL, Header: make(http.Header)}
proxy.getDirector()(&req)
if err != nil {
log.Fatal(4, err.Error())
}
Convey("Match header value after decryption", func() {
So(req.Header.Get("Authorization"), ShouldEqual, "Bearer xf5yhfkpsnmgo")
})
})
})
}
+11
View File
@@ -3,7 +3,9 @@ package api
import (
"fmt"
"net/http"
"runtime"
"strconv"
"strings"
"time"
m "github.com/grafana/grafana/pkg/models"
@@ -55,6 +57,15 @@ func (hs *HTTPServer) RenderToPng(c *m.ReqContext) {
return
}
if err != nil && err == rendering.ErrPhantomJSNotInstalled {
if strings.HasPrefix(runtime.GOARCH, "arm") {
c.Handle(500, "Rendering failed - PhantomJS isn't included in arm build per default", err)
} else {
c.Handle(500, "Rendering failed - PhantomJS isn't installed correctly", err)
}
return
}
if err != nil {
c.Handle(500, "Rendering failed.", err)
return
@@ -1,9 +1,10 @@
package api
package routing
import (
"net/http"
"strings"
macaron "gopkg.in/macaron.v1"
"gopkg.in/macaron.v1"
)
type Router interface {
@@ -14,16 +15,34 @@ type Router interface {
// RouteRegister allows you to add routes and macaron.Handlers
// that the web server should serve.
type RouteRegister interface {
// Get adds a list of handlers to a given route with a GET HTTP verb
Get(string, ...macaron.Handler)
// Post adds a list of handlers to a given route with a POST HTTP verb
Post(string, ...macaron.Handler)
// Delete adds a list of handlers to a given route with a DELETE HTTP verb
Delete(string, ...macaron.Handler)
// Put adds a list of handlers to a given route with a PUT HTTP verb
Put(string, ...macaron.Handler)
// Patch adds a list of handlers to a given route with a PATCH HTTP verb
Patch(string, ...macaron.Handler)
// Any adds a list of handlers to a given route with any HTTP verb
Any(string, ...macaron.Handler)
// Group allows you to pass a function that can add multiple routes
// with a shared prefix route.
Group(string, func(RouteRegister), ...macaron.Handler)
Register(Router) *macaron.Router
// Insert adds more routes to an existing Group.
Insert(string, func(RouteRegister), ...macaron.Handler)
// Register iterates over all routes added to the RouteRegister
// and add them to the `Router` pass as an parameter.
Register(Router)
}
type RegisterNamedMiddleware func(name string) macaron.Handler
@@ -52,6 +71,24 @@ type routeRegister struct {
groups []*routeRegister
}
func (rr *routeRegister) Insert(pattern string, fn func(RouteRegister), handlers ...macaron.Handler) {
//loop over all groups at current level
for _, g := range rr.groups {
// apply routes if the prefix matches the pattern
if g.prefix == pattern {
g.Group("", fn)
break
}
// go down one level if the prefix can be find in the pattern
if strings.HasPrefix(pattern, g.prefix) {
g.Insert(pattern, fn)
}
}
}
func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handlers ...macaron.Handler) {
group := &routeRegister{
prefix: rr.prefix + pattern,
@@ -64,7 +101,7 @@ func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handle
rr.groups = append(rr.groups, group)
}
func (rr *routeRegister) Register(router Router) *macaron.Router {
func (rr *routeRegister) Register(router Router) {
for _, r := range rr.routes {
// GET requests have to be added to macaron routing using Get()
// Otherwise HEAD requests will not be allowed.
@@ -79,8 +116,6 @@ func (rr *routeRegister) Register(router Router) *macaron.Router {
for _, g := range rr.groups {
g.Register(router)
}
return &macaron.Router{}
}
func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {
@@ -92,6 +127,12 @@ func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handl
h = append(h, rr.subfixHandlers...)
h = append(h, handlers...)
for _, r := range rr.routes {
if r.pattern == rr.prefix+pattern && r.method == method {
panic("cannot add duplicate route")
}
}
rr.routes = append(rr.routes, route{
method: method,
pattern: rr.prefix + pattern,
@@ -1,11 +1,11 @@
package api
package routing
import (
"net/http"
"strconv"
"testing"
macaron "gopkg.in/macaron.v1"
"gopkg.in/macaron.v1"
)
type fakeRouter struct {
@@ -33,7 +33,7 @@ func (fr *fakeRouter) Get(pattern string, handlers ...macaron.Handler) *macaron.
}
func emptyHandlers(n int) []macaron.Handler {
res := []macaron.Handler{}
var res []macaron.Handler
for i := 1; n >= i; i++ {
res = append(res, emptyHandler(strconv.Itoa(i)))
}
@@ -138,7 +138,78 @@ func TestRouteGroupedRegister(t *testing.T) {
}
}
}
func TestRouteGroupInserting(t *testing.T) {
testTable := []route{
{method: http.MethodGet, pattern: "/api/", handlers: emptyHandlers(1)},
{method: http.MethodPost, pattern: "/api/group/endpoint", handlers: emptyHandlers(1)},
{method: http.MethodGet, pattern: "/api/group/inserted", handlers: emptyHandlers(1)},
{method: http.MethodDelete, pattern: "/api/inserted-endpoint", handlers: emptyHandlers(1)},
}
// Setup
rr := NewRouteRegister()
rr.Group("/api", func(api RouteRegister) {
api.Get("/", emptyHandler("1"))
api.Group("/group", func(group RouteRegister) {
group.Post("/endpoint", emptyHandler("1"))
})
})
rr.Insert("/api", func(api RouteRegister) {
api.Delete("/inserted-endpoint", emptyHandler("1"))
})
rr.Insert("/api/group", func(group RouteRegister) {
group.Get("/inserted", emptyHandler("1"))
})
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Fatalf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
func TestDuplicateRoutShouldPanic(t *testing.T) {
defer func() {
if recover() != "cannot add duplicate route" {
t.Errorf("Should cause panic if duplicate routes are added ")
}
}()
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
rr.Get("/api", emptyHandler("1"))
rr.Get("/api", emptyHandler("1"))
fr := &fakeRouter{}
rr.Register(fr)
}
func TestNamedMiddlewareRouteRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(2)},
-4
View File
@@ -14,7 +14,6 @@ import (
"net/http"
_ "net/http/pprof"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/setting"
@@ -88,9 +87,6 @@ func main() {
err := server.Run()
trace.Stop()
log.Close()
server.Exit(err)
}
+6 -3
View File
@@ -12,6 +12,7 @@ import (
"time"
"github.com/facebookgo/inject"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/registry"
@@ -61,8 +62,8 @@ type GrafanaServerImpl struct {
shutdownReason string
shutdownInProgress bool
RouteRegister api.RouteRegister `inject:""`
HttpServer *api.HTTPServer `inject:""`
RouteRegister routing.RouteRegister `inject:""`
HttpServer *api.HTTPServer `inject:""`
}
func (g *GrafanaServerImpl) Run() error {
@@ -75,7 +76,7 @@ func (g *GrafanaServerImpl) Run() error {
serviceGraph := inject.Graph{}
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
serviceGraph.Provide(&inject.Object{Value: g.cfg})
serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
// self registered services
services := registry.GetServices()
@@ -184,6 +185,8 @@ func (g *GrafanaServerImpl) Exit(reason error) {
}
g.log.Error("Server shutdown", "reason", reason)
log.Close()
os.Exit(code)
}
+2 -1
View File
@@ -308,6 +308,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
} else {
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
}
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
a.log.Info("Searching for user's groups", "filter", filter)
@@ -348,7 +349,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
}
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
if name == "DN" {
if strings.ToLower(name) == "dn" {
return result.Entries[n].DN
}
for _, attr := range result.Entries[n].Attributes {
+6
View File
@@ -9,6 +9,7 @@ import (
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type AuthOptions struct {
@@ -34,6 +35,11 @@ func getApiKey(c *m.ReqContext) string {
return key
}
username, password, err := util.DecodeBasicAuthHeader(header)
if err == nil && username == "api_key" {
return password
}
return ""
}
+8 -12
View File
@@ -2,6 +2,7 @@ package middleware
import (
"fmt"
"net"
"net/mail"
"reflect"
"strings"
@@ -28,7 +29,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
}
// if auth proxy ip(s) defined, check if request comes from one of those
if err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil {
if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
ctx.Handle(407, "Proxy authentication required", err)
return true
}
@@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
return nil
}
// Multiple ip addresses? Right-most IP address is the IP address of the most recent proxy
if strings.Contains(remoteAddr, ",") {
sourceIPs := strings.Split(remoteAddr, ",")
remoteAddr = strings.TrimSpace(sourceIPs[len(sourceIPs)-1])
}
remoteAddr = strings.TrimPrefix(remoteAddr, "[")
remoteAddr = strings.TrimSuffix(remoteAddr, "]")
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
sourceIP, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return err
}
// Compare allowed IP addresses to actual address
for _, proxyIP := range proxies {
if remoteAddr == strings.TrimSpace(proxyIP) {
if sourceIP == strings.TrimSpace(proxyIP) {
return nil
}
}
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, remoteAddr)
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
}
+24 -57
View File
@@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) {
setting.BasicAuthEnabled = true
authHeader := util.GetBasicAuthHeader("myUser", "myPass")
sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec()
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should init middleware context with user", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
@@ -128,6 +128,28 @@ func TestMiddlewareContext(t *testing.T) {
})
})
middlewareScenario("Valid api key via Basic auth", func(sc *scenarioContext) {
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
return nil
})
authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9")
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should return 200", func() {
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("Should init middleware context", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
So(sc.context.OrgId, ShouldEqual, 12)
So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR)
})
})
middlewareScenario("UserId in session", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
@@ -293,61 +315,6 @@ func TestMiddlewareContext(t *testing.T) {
})
})
middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is not trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.1, 192.168.1.2")
sc.exec()
Convey("should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.1.2 is not from the authentication proxy")
})
})
middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.2, 192.168.1.1")
sc.exec()
Convey("Should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 33)
So(sc.context.OrgId, ShouldEqual, 4)
})
})
middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
@@ -473,7 +440,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
return sc
}
func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext {
func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
sc.authHeader = authHeader
return sc
}
+19 -5
View File
@@ -34,23 +34,37 @@ func GetServices() []*Descriptor {
return services
}
// Service interface is the lowest common shape that services
// are expected to forfill to be started within Grafana.
type Service interface {
// Init is called by Grafana main process which gives the service
// the possibility do some initial work before its started. Things
// like adding routes, bus handlers should be done in the Init function
Init() error
}
// Useful for alerting service
// CanBeDisabled allows the services to decide if it should
// be started or not by itself. This is useful for services
// that might not always be started, ex alerting.
// This will be called after `Init()`.
type CanBeDisabled interface {
// IsDisabled should return a bool saying if it can be started or not.
IsDisabled() bool
}
// BackgroundService should be implemented for services that have
// long running tasks in the background.
type BackgroundService interface {
// Run starts the background process of the service after `Init` have been called
// on all services. The `context.Context` passed into the function should be used
// to subscribe to ctx.Done() so the service can be notified when Grafana shuts down.
Run(ctx context.Context) error
}
type HasInitPriority interface {
GetInitPriority() Priority
}
// IsDisabled takes an service and return true if its disabled
func IsDisabled(srv Service) bool {
canBeDisabled, ok := srv.(CanBeDisabled)
return ok && canBeDisabled.IsDisabled()
+4 -1
View File
@@ -131,7 +131,10 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
return err
}
n.log.Info("uploaded", "url", context.ImagePublicUrl)
if context.ImagePublicUrl != "" {
n.log.Info("uploaded screenshot of alert to external image store", "url", context.ImagePublicUrl)
}
return nil
}
+6 -5
View File
@@ -35,11 +35,12 @@ type PostParams struct {
}
type DeleteParams struct {
Id int64 `json:"id"`
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
RegionId int64 `json:"regionId"`
OrgId int64
Id int64
AlertId int64
DashboardId int64
PanelId int64
RegionId int64
}
var repositoryInstance Repository
+11 -1
View File
@@ -57,8 +57,10 @@ func (srv *CleanUpService) cleanUpTmpFiles() {
}
var toDelete []os.FileInfo
var now = time.Now()
for _, file := range files {
if file.ModTime().AddDate(0, 0, 1).Before(time.Now()) {
if srv.shouldCleanupTempFile(file.ModTime(), now) {
toDelete = append(toDelete, file)
}
}
@@ -74,6 +76,14 @@ func (srv *CleanUpService) cleanUpTmpFiles() {
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
}
func (srv *CleanUpService) shouldCleanupTempFile(filemtime time.Time, now time.Time) bool {
if srv.Cfg.TempDataLifetime == 0 {
return false
}
return filemtime.Add(srv.Cfg.TempDataLifetime).Before(now)
}
func (srv *CleanUpService) deleteExpiredSnapshots() {
cmd := m.DeleteExpiredSnapshotsCommand{}
if err := bus.Dispatch(&cmd); err != nil {
+41
View File
@@ -0,0 +1,41 @@
package cleanup
import (
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"testing"
"time"
)
func TestCleanUpTmpFiles(t *testing.T) {
Convey("Cleanup service tests", t, func() {
cfg := setting.Cfg{}
cfg.TempDataLifetime, _ = time.ParseDuration("24h")
service := CleanUpService{
Cfg: &cfg,
}
now := time.Now()
secondAgo := now.Add(-time.Second)
twoDaysAgo := now.Add(-time.Second * 3600 * 24 * 2)
weekAgo := now.Add(-time.Second * 3600 * 24 * 7)
Convey("Should not cleanup recent files", func() {
So(service.shouldCleanupTempFile(secondAgo, now), ShouldBeFalse)
})
Convey("Should cleanup older files", func() {
So(service.shouldCleanupTempFile(twoDaysAgo, now), ShouldBeTrue)
})
Convey("After increasing temporary files lifetime, older files should be kept", func() {
cfg.TempDataLifetime, _ = time.ParseDuration("1000h")
So(service.shouldCleanupTempFile(weekAgo, now), ShouldBeFalse)
})
Convey("If lifetime is 0, files should never be cleaned up", func() {
cfg.TempDataLifetime = 0
So(service.shouldCleanupTempFile(weekAgo, now), ShouldBeFalse)
})
})
}
+1 -1
View File
@@ -83,7 +83,7 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D
for _, p := range acl {
// user match
if !g.user.IsAnonymous {
if !g.user.IsAnonymous && p.UserId > 0 {
if p.UserId == g.user.UserId && p.Permission >= permission {
return true, nil
}
+14 -3
View File
@@ -28,7 +28,7 @@ func TestGuardianAdmin(t *testing.T) {
Convey("Guardian admin org role tests", t, func() {
orgRoleScenario("Given user has admin org role", t, m.ROLE_ADMIN, func(sc *scenarioContext) {
// dashboard has default permissions
sc.defaultPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
sc.defaultPermissionScenario(USER, FULL_ACCESS)
// dashboard has user with permission
sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
@@ -76,6 +76,9 @@ func TestGuardianAdmin(t *testing.T) {
func TestGuardianEditor(t *testing.T) {
Convey("Guardian editor org role tests", t, func() {
orgRoleScenario("Given user has editor org role", t, m.ROLE_EDITOR, func(sc *scenarioContext) {
// dashboard has default permissions
sc.defaultPermissionScenario(USER, EDITOR_ACCESS)
// dashboard has user with permission
sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
@@ -122,6 +125,9 @@ func TestGuardianEditor(t *testing.T) {
func TestGuardianViewer(t *testing.T) {
Convey("Guardian viewer org role tests", t, func() {
orgRoleScenario("Given user has viewer org role", t, m.ROLE_VIEWER, func(sc *scenarioContext) {
// dashboard has default permissions
sc.defaultPermissionScenario(USER, VIEWER_ACCESS)
// dashboard has user with permission
sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
@@ -162,10 +168,15 @@ func TestGuardianViewer(t *testing.T) {
sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_ACCESS)
sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, VIEWER_ACCESS)
})
apiKeyScenario("Given api key with viewer role", t, m.ROLE_VIEWER, func(sc *scenarioContext) {
// dashboard has default permissions
sc.defaultPermissionScenario(VIEWER, VIEWER_ACCESS)
})
})
}
func (sc *scenarioContext) defaultPermissionScenario(pt permissionType, permission m.PermissionType, flag permissionFlags) {
func (sc *scenarioContext) defaultPermissionScenario(pt permissionType, flag permissionFlags) {
_, callerFile, callerLine, _ := runtime.Caller(1)
sc.callerFile = callerFile
sc.callerLine = callerLine
@@ -267,7 +278,7 @@ func (sc *scenarioContext) verifyExpectedPermissionsFlags() {
actualFlag = NO_ACCESS
}
if sc.expectedFlags&actualFlag != sc.expectedFlags {
if actualFlag&sc.expectedFlags != actualFlag {
sc.reportFailure(tc, sc.expectedFlags.String(), actualFlag.String())
}
@@ -48,6 +48,27 @@ func orgRoleScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc
})
}
func apiKeyScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc) {
user := &m.SignedInUser{
UserId: 0,
OrgId: orgID,
OrgRole: role,
ApiKeyId: 10,
}
guard := New(dashboardID, orgID, user)
sc := &scenarioContext{
t: t,
orgRoleScenario: desc,
givenUser: user,
givenDashboardID: dashboardID,
g: guard,
}
Convey(desc, func() {
fn(sc)
})
}
func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
bus.ClearBusHandlers()
+1
View File
@@ -10,6 +10,7 @@ import (
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
var ErrNoRenderer = errors.New("No renderer plugin found nor is an external render server configured")
var ErrPhantomJSNotInstalled = errors.New("PhantomJS executable not found")
type Opts struct {
Width int
+5
View File
@@ -24,6 +24,11 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
url := rs.getURL(opts.Path)
binPath, _ := filepath.Abs(filepath.Join(rs.Cfg.PhantomDir, executable))
if _, err := os.Stat(binPath); os.IsNotExist(err) {
rs.log.Error("executable not found", "executable", binPath)
return nil, ErrPhantomJSNotInstalled
}
scriptPath, _ := filepath.Abs(filepath.Join(rs.Cfg.PhantomDir, "render.js"))
pngPath := rs.getFilePathForNewImage()
+10 -9
View File
@@ -238,18 +238,19 @@ func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
queryParams []interface{}
)
sqlog.Info("delete", "orgId", params.OrgId)
if params.RegionId != 0 {
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE region_id = ?)"
sql = "DELETE FROM annotation WHERE region_id = ?"
queryParams = []interface{}{params.RegionId}
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE region_id = ? AND org_id = ?)"
sql = "DELETE FROM annotation WHERE region_id = ? AND org_id = ?"
queryParams = []interface{}{params.RegionId, params.OrgId}
} else if params.Id != 0 {
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ?)"
sql = "DELETE FROM annotation WHERE id = ?"
queryParams = []interface{}{params.Id}
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ? AND org_id = ?)"
sql = "DELETE FROM annotation WHERE id = ? AND org_id = ?"
queryParams = []interface{}{params.Id, params.OrgId}
} else {
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ?)"
sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
queryParams = []interface{}{params.DashboardId, params.PanelId}
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?)"
sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ? AND org_id = ?"
queryParams = []interface{}{params.DashboardId, params.PanelId, params.OrgId}
}
if _, err := sess.Exec(annoTagSql, queryParams...); err != nil {
+1 -1
View File
@@ -268,7 +268,7 @@ func TestAnnotations(t *testing.T) {
annotationId := items[0].Id
err = repo.Delete(&annotations.DeleteParams{Id: annotationId})
err = repo.Delete(&annotations.DeleteParams{Id: annotationId, OrgId: 1})
So(err, ShouldBeNil)
items, err = repo.Find(query)
+10 -10
View File
@@ -27,18 +27,18 @@ func startSession(ctx context.Context, engine *xorm.Engine, beginTran bool) (*DB
var sess *DBSession
sess, ok := value.(*DBSession)
if !ok {
newSess := &DBSession{Session: engine.NewSession()}
if beginTran {
err := newSess.Begin()
if err != nil {
return nil, err
}
}
return newSess, nil
if ok {
return sess, nil
}
return sess, nil
newSess := &DBSession{Session: engine.NewSession()}
if beginTran {
err := newSess.Begin()
if err != nil {
return nil, err
}
}
return newSess, nil
}
func withDbSession(ctx context.Context, callback dbTransactionFunc) error {
+59 -1
View File
@@ -26,7 +26,7 @@ import (
_ "github.com/grafana/grafana/pkg/tsdb/mssql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
sqlite3 "github.com/mattn/go-sqlite3"
)
var (
@@ -56,6 +56,64 @@ type SqlStore struct {
skipEnsureAdmin bool
}
// NewSession returns a new DBSession
func (ss *SqlStore) NewSession() *DBSession {
return &DBSession{Session: ss.engine.NewSession()}
}
// WithDbSession calls the callback with an session attached to the context.
func (ss *SqlStore) WithDbSession(ctx context.Context, callback dbTransactionFunc) error {
sess, err := startSession(ctx, ss.engine, false)
if err != nil {
return err
}
return callback(sess)
}
// WithTransactionalDbSession calls the callback with an session within a transaction
func (ss *SqlStore) WithTransactionalDbSession(ctx context.Context, callback dbTransactionFunc) error {
return ss.inTransactionWithRetryCtx(ctx, callback, 0)
}
func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, retry int) error {
sess, err := startSession(ctx, ss.engine, true)
if err != nil {
return err
}
defer sess.Close()
err = callback(sess)
// special handling of database locked errors for sqlite, then we can retry 3 times
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
if sqlError.Code == sqlite3.ErrLocked {
sess.Rollback()
time.Sleep(time.Millisecond * time.Duration(10))
sqlog.Info("Database table locked, sleeping then retrying", "retry", retry)
return ss.inTransactionWithRetryCtx(ctx, callback, retry+1)
}
}
if err != nil {
sess.Rollback()
return err
} else if err = sess.Commit(); err != nil {
return err
}
if len(sess.events) > 0 {
for _, e := range sess.events {
if err = bus.Publish(e); err != nil {
log.Error(3, "Failed to publish event after commit", err)
}
}
}
return nil
}
func (ss *SqlStore) Init() error {
ss.log = log.New("sqlstore")
ss.readConfig()
+4
View File
@@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
"time"
)
type Scheme string
@@ -195,6 +196,8 @@ type Cfg struct {
PhantomDir string
RendererUrl string
DisableBruteForceLoginProtection bool
TempDataLifetime time.Duration
}
type CommandLineArgs struct {
@@ -637,6 +640,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.RendererUrl = renderSec.Key("server_url").String()
cfg.ImagesDir = filepath.Join(DataPath, "png")
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
analytics := iniFile.Section("analytics")
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)