Merge branch 'master' into mssql_datasource

This commit is contained in:
Marcus Efraimsson
2018-03-13 16:03:02 +01:00
2626 changed files with 339805 additions and 172731 deletions

View File

@@ -4,12 +4,11 @@ import (
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func AdminGetSettings(c *middleware.Context) {
func AdminGetSettings(c *m.ReqContext) {
settings := make(map[string]interface{})
for _, section := range setting.Cfg.Sections() {
@@ -30,7 +29,7 @@ func AdminGetSettings(c *middleware.Context) {
c.JSON(200, settings)
}
func AdminGetStats(c *middleware.Context) {
func AdminGetStats(c *m.ReqContext) {
statsQuery := m.GetAdminStatsQuery{}

View File

@@ -4,12 +4,11 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) {
cmd := m.CreateUserCommand{
Login: form.Login,
Email: form.Email,
@@ -47,7 +46,7 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
c.JSON(200, result)
}
func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) {
func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) {
userId := c.ParamsInt64(":id")
if len(form.Password) < 4 {
@@ -77,7 +76,7 @@ func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPas
c.JsonOK("User password updated")
}
func AdminUpdateUserPermissions(c *middleware.Context, form dtos.AdminUpdateUserPermissionsForm) {
func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
userId := c.ParamsInt64(":id")
cmd := m.UpdateUserPermissionsCommand{
@@ -93,7 +92,7 @@ func AdminUpdateUserPermissions(c *middleware.Context, form dtos.AdminUpdateUser
c.JsonOK("User permissions updated")
}
func AdminDeleteUser(c *middleware.Context) {
func AdminDeleteUser(c *m.ReqContext) {
userId := c.ParamsInt64(":id")
cmd := m.DeleteUserCommand{UserId: userId}

View File

@@ -5,14 +5,14 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/guardian"
)
func ValidateOrgAlert(c *middleware.Context) {
func ValidateOrgAlert(c *m.ReqContext) {
id := c.ParamsInt64(":alertId")
query := models.GetAlertByIdQuery{Id: id}
query := m.GetAlertByIdQuery{Id: id}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(404, "Alert not found", nil)
@@ -25,14 +25,14 @@ func ValidateOrgAlert(c *middleware.Context) {
}
}
func GetAlertStatesForDashboard(c *middleware.Context) Response {
func GetAlertStatesForDashboard(c *m.ReqContext) Response {
dashboardId := c.QueryInt64("dashboardId")
if dashboardId == 0 {
return ApiError(400, "Missing query parameter dashboardId", nil)
}
query := models.GetAlertStatesForDashboardQuery{
query := m.GetAlertStatesForDashboardQuery{
OrgId: c.OrgId,
DashboardId: c.QueryInt64("dashboardId"),
}
@@ -45,12 +45,13 @@ func GetAlertStatesForDashboard(c *middleware.Context) Response {
}
// GET /api/alerts
func GetAlerts(c *middleware.Context) Response {
query := models.GetAlertsQuery{
func GetAlerts(c *m.ReqContext) Response {
query := m.GetAlertsQuery{
OrgId: c.OrgId,
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"),
User: c.SignedInUser,
}
states := c.QueryStrings("state")
@@ -62,47 +63,15 @@ func GetAlerts(c *middleware.Context) Response {
return ApiError(500, "List alerts failed", err)
}
dashboardIds := make([]int64, 0)
alertDTOs := make([]*dtos.AlertRule, 0)
for _, alert := range query.Result {
dashboardIds = append(dashboardIds, alert.DashboardId)
alertDTOs = append(alertDTOs, &dtos.AlertRule{
Id: alert.Id,
DashboardId: alert.DashboardId,
PanelId: alert.PanelId,
Name: alert.Name,
Message: alert.Message,
State: alert.State,
NewStateDate: alert.NewStateDate,
ExecutionError: alert.ExecutionError,
EvalData: alert.EvalData,
})
alert.Url = m.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
}
dashboardsQuery := models.GetDashboardsQuery{
DashboardIds: dashboardIds,
}
if len(alertDTOs) > 0 {
if err := bus.Dispatch(&dashboardsQuery); err != nil {
return ApiError(500, "List alerts failed", err)
}
}
//TODO: should be possible to speed this up with lookup table
for _, alert := range alertDTOs {
for _, dash := range dashboardsQuery.Result {
if alert.DashboardId == dash.Id {
alert.DashbboardUri = "db/" + dash.Slug
}
}
}
return Json(200, alertDTOs)
return Json(200, query.Result)
}
// POST /api/alerts/test
func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
if _, idErr := dto.Dashboard.Get("id").Int64(); idErr != nil {
return ApiError(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil)
}
@@ -144,9 +113,9 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
}
// GET /api/alerts/:id
func GetAlert(c *middleware.Context) Response {
func GetAlert(c *m.ReqContext) Response {
id := c.ParamsInt64(":alertId")
query := models.GetAlertByIdQuery{Id: id}
query := m.GetAlertByIdQuery{Id: id}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "List alerts failed", err)
@@ -155,30 +124,12 @@ func GetAlert(c *middleware.Context) Response {
return Json(200, &query.Result)
}
// DEL /api/alerts/:id
func DelAlert(c *middleware.Context) Response {
alertId := c.ParamsInt64(":alertId")
if alertId == 0 {
return ApiError(401, "Failed to parse alertid", nil)
}
cmd := models.DeleteAlertCommand{AlertId: alertId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete alert", err)
}
var resp = map[string]interface{}{"alertId": alertId}
return Json(200, resp)
}
func GetAlertNotifiers(c *middleware.Context) Response {
func GetAlertNotifiers(c *m.ReqContext) Response {
return Json(200, alerting.GetNotifiers())
}
func GetAlertNotifications(c *middleware.Context) Response {
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
func GetAlertNotifications(c *m.ReqContext) Response {
query := &m.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
if err := bus.Dispatch(query); err != nil {
return ApiError(500, "Failed to get alert notifications", err)
@@ -200,8 +151,8 @@ func GetAlertNotifications(c *middleware.Context) Response {
return Json(200, result)
}
func GetAlertNotificationById(c *middleware.Context) Response {
query := &models.GetAlertNotificationsQuery{
func GetAlertNotificationById(c *m.ReqContext) Response {
query := &m.GetAlertNotificationsQuery{
OrgId: c.OrgId,
Id: c.ParamsInt64("notificationId"),
}
@@ -213,7 +164,7 @@ func GetAlertNotificationById(c *middleware.Context) Response {
return Json(200, query.Result)
}
func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response {
func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
@@ -223,7 +174,7 @@ func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotifi
return Json(200, cmd.Result)
}
func UpdateAlertNotification(c *middleware.Context, cmd models.UpdateAlertNotificationCommand) Response {
func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
@@ -233,8 +184,8 @@ func UpdateAlertNotification(c *middleware.Context, cmd models.UpdateAlertNotifi
return Json(200, cmd.Result)
}
func DeleteAlertNotification(c *middleware.Context) Response {
cmd := models.DeleteAlertNotificationCommand{
func DeleteAlertNotification(c *m.ReqContext) Response {
cmd := m.DeleteAlertNotificationCommand{
OrgId: c.OrgId,
Id: c.ParamsInt64("notificationId"),
}
@@ -247,7 +198,7 @@ func DeleteAlertNotification(c *middleware.Context) Response {
}
//POST /api/alert-notifications/test
func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) Response {
func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Response {
cmd := &alerting.NotificationTestCommand{
Name: dto.Name,
Type: dto.Type,
@@ -255,7 +206,7 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R
}
if err := bus.Dispatch(cmd); err != nil {
if err == models.ErrSmtpNotEnabled {
if err == m.ErrSmtpNotEnabled {
return ApiError(412, err.Error(), err)
}
return ApiError(500, "Failed to send alert notifications", err)
@@ -265,9 +216,25 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R
}
//POST /api/alerts/:alertId/pause
func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
alertId := c.ParamsInt64("alertId")
cmd := models.PauseAlertCommand{
query := m.GetAlertByIdQuery{Id: alertId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Get Alert failed", err)
}
guardian := guardian.New(query.Result.DashboardId, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
if err != nil {
return ApiError(500, "Error while checking permissions for Alert", err)
}
return ApiError(403, "Access denied to this dashboard and alert", nil)
}
cmd := m.PauseAlertCommand{
OrgId: c.OrgId,
AlertIds: []int64{alertId},
Paused: dto.Paused,
@@ -277,25 +244,25 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
return ApiError(500, "", err)
}
var response models.AlertStateType = models.AlertStatePending
pausedState := "un paused"
var response m.AlertStateType = m.AlertStatePending
pausedState := "un-paused"
if cmd.Paused {
response = models.AlertStatePaused
response = m.AlertStatePaused
pausedState = "paused"
}
result := map[string]interface{}{
"alertId": alertId,
"state": response,
"message": "alert " + pausedState,
"message": "Alert " + pausedState,
}
return Json(200, result)
}
//POST /api/admin/pause-all-alerts
func PauseAllAlerts(c *middleware.Context, dto dtos.PauseAllAlertsCommand) Response {
updateCmd := models.PauseAllAlertCommand{
func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
updateCmd := m.PauseAllAlertCommand{
Paused: dto.Paused,
}
@@ -303,10 +270,10 @@ func PauseAllAlerts(c *middleware.Context, dto dtos.PauseAllAlertsCommand) Respo
return ApiError(500, "Failed to pause alerts", err)
}
var response models.AlertStateType = models.AlertStatePending
var response m.AlertStateType = m.AlertStatePending
pausedState := "un paused"
if updateCmd.Paused {
response = models.AlertStatePaused
response = m.AlertStatePaused
pausedState = "paused"
}

96
pkg/api/alerting_test.go Normal file
View File

@@ -0,0 +1,96 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertingApiEndpoint(t *testing.T) {
Convey("Given an alert in a dashboard with an acl", t, func() {
singleAlert := &m.Alert{Id: 1, DashboardId: 1, Name: "singlealert"}
bus.AddHandler("test", func(query *m.GetAlertByIdQuery) error {
query.Result = singleAlert
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
Convey("When user is editor and not in the ACL", func() {
Convey("Should not be able to pause the alert", func() {
cmd := dtos.PauseAlertCommand{
AlertId: 1,
Paused: true,
}
postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
CallPauseAlert(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is editor and dashboard has default ACL", func() {
aclMockResp = []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
Convey("Should be able to pause the alert", func() {
cmd := dtos.PauseAlertCommand{
AlertId: 1,
Paused: true,
}
postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
CallPauseAlert(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
}
func CallPauseAlert(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.PauseAlertCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func postAlertScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PauseAlertCommand, 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 PauseAlert(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -6,12 +6,13 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
func GetAnnotations(c *middleware.Context) Response {
func GetAnnotations(c *m.ReqContext) Response {
query := &annotations.ItemQuery{
From: c.QueryInt64("from") / 1000,
@@ -50,7 +51,11 @@ func (e *CreateAnnotationError) Error() string {
return e.message
}
func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
if canSave, err := canSaveByDashboardId(c, cmd.DashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
repo := annotations.GetRepository()
if cmd.Text == "" {
@@ -119,7 +124,7 @@ func formatGraphiteAnnotation(what string, data string) string {
return text
}
func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotationsCmd) Response {
func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd) Response {
repo := annotations.GetRepository()
if cmd.What == "" {
@@ -173,11 +178,15 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati
})
}
func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response {
func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
annotationId := c.ParamsInt64(":annotationId")
repo := annotations.GetRepository()
if resp := canSave(c, repo, annotationId); resp != nil {
return resp
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
@@ -208,7 +217,7 @@ func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Resp
return ApiSuccess("Annotation updated")
}
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
repo := annotations.GetRepository()
err := repo.Delete(&annotations.DeleteParams{
@@ -224,10 +233,14 @@ func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Res
return ApiSuccess("Annotations deleted")
}
func DeleteAnnotationById(c *middleware.Context) Response {
func DeleteAnnotationById(c *m.ReqContext) Response {
repo := annotations.GetRepository()
annotationId := c.ParamsInt64(":annotationId")
if resp := canSave(c, repo, annotationId); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{
Id: annotationId,
})
@@ -239,10 +252,14 @@ func DeleteAnnotationById(c *middleware.Context) Response {
return ApiSuccess("Annotation deleted")
}
func DeleteAnnotationRegion(c *middleware.Context) Response {
func DeleteAnnotationRegion(c *m.ReqContext) Response {
repo := annotations.GetRepository()
regionId := c.ParamsInt64(":regionId")
if resp := canSave(c, repo, regionId); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{
RegionId: regionId,
})
@@ -253,3 +270,50 @@ func DeleteAnnotationRegion(c *middleware.Context) Response {
return ApiSuccess("Annotation region deleted")
}
func canSaveByDashboardId(c *m.ReqContext, dashboardId int64) (bool, error) {
if dashboardId == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) {
return false, nil
}
if dashboardId > 0 {
guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
return false, err
}
}
return true, nil
}
func canSave(c *m.ReqContext, repo annotations.Repository, annotationId int64) Response {
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationId, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return ApiError(500, "Could not find annotation to update", err)
}
dashboardId := items[0].DashboardId
if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
return nil
}
func canSaveByRegionId(c *m.ReqContext, repo annotations.Repository, regionId int64) Response {
items, err := repo.Find(&annotations.ItemQuery{RegionId: regionId, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return ApiError(500, "Could not find annotation to update", err)
}
dashboardId := items[0].DashboardId
if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
return nil
}

241
pkg/api/annotations_test.go Normal file
View File

@@ -0,0 +1,241 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
. "github.com/smartystreets/goconvey/convey"
)
func TestAnnotationsApiEndpoint(t *testing.T) {
Convey("Given an annotation without a dashboard id", t, func() {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
}
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
Convey("Should not be allowed to save an annotation", 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, 403)
})
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, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
Convey("Should be able to save an annotation", 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)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
Convey("Given an annotation with a dashboard id and the dashboard does not have an acl", t, func() {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
DashboardId: 1,
PanelId: 1,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
Id: 1,
}
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
Convey("Should not be allowed to save an annotation", 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, 403)
})
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, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
Convey("Should be able to save an annotation", 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)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
}
type fakeAnnotationsRepo struct {
}
func (repo *fakeAnnotationsRepo) Delete(params *annotations.DeleteParams) error {
return nil
}
func (repo *fakeAnnotationsRepo) Save(item *annotations.Item) error {
item.Id = 1
return nil
}
func (repo *fakeAnnotationsRepo) Update(item *annotations.Item) error {
return nil
}
func (repo *fakeAnnotationsRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
annotations := []*annotations.ItemDTO{{Id: 1}}
return annotations, nil
}
var fakeAnnoRepo *fakeAnnotationsRepo
func postAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PostAnnotationsCmd, 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 PostAnnotation(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func putAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.UpdateAnnotationsCmd, 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 UpdateAnnotation(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -15,6 +15,8 @@ func (hs *HttpServer) registerRoutes() {
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl()
redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl()
quota := middleware.Quota
bind := binding.Bind
@@ -63,9 +65,13 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/plugins/:id/edit", reqSignedIn, Index)
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
r.Get("/dashboard/*", reqSignedIn, Index)
r.Get("/d/:uid/:slug", reqSignedIn, Index)
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index)
r.Get("/dashboard/script/*", reqSignedIn, Index)
r.Get("/dashboard-solo/snapshot/*", Index)
r.Get("/dashboard-solo/*", reqSignedIn, Index)
r.Get("/d-solo/:uid/:slug", reqSignedIn, Index)
r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloUrl, Index)
r.Get("/dashboard-solo/script/*", reqSignedIn, Index)
r.Get("/import/dashboard", reqSignedIn, Index)
r.Get("/dashboards/", reqSignedIn, Index)
r.Get("/dashboards/*", reqSignedIn, Index)
@@ -100,7 +106,7 @@ func (hs *HttpServer) registerRoutes() {
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/api/snapshot/shared-options/", GetSharingOptions)
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
r.Get("/api/snapshots-delete/:key", reqEditorRole, DeleteDashboardSnapshot)
r.Get("/api/snapshots-delete/:key", reqEditorRole, wrap(DeleteDashboardSnapshot))
// api renew session based on remember cookie
r.Get("/api/login/ping", quota("session"), LoginApiPing)
@@ -144,11 +150,11 @@ func (hs *HttpServer) registerRoutes() {
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamById))
teamsRoute.Get("/search", wrap(SearchTeams))
teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
}, reqOrgAdmin)
@@ -240,14 +246,35 @@ func (hs *HttpServer) registerRoutes() {
apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
// Folders
apiRoute.Group("/folders", func(folderRoute RouteRegister) {
folderRoute.Get("/", wrap(GetFolders))
folderRoute.Get("/id/:id", wrap(GetFolderById))
folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) {
folderUidRoute.Get("/", wrap(GetFolderByUid))
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
folderUidRoute.Delete("/", wrap(DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute RouteRegister) {
folderPermissionRoute.Get("/", wrap(GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions))
})
})
})
// Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid))
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard))
dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
dashboardRoute.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
dashboardRoute.Get("/tags", GetDashboardTags)
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
@@ -255,12 +282,11 @@ func (hs *HttpServer) registerRoutes() {
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
dashIdRoute.Post("/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
aclRoute.Get("/", wrap(GetDashboardAclList))
aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute RouteRegister) {
dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions))
})
})
})
@@ -317,8 +343,8 @@ func (hs *HttpServer) registerRoutes() {
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
annotationsRoute.Post("/graphite", bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
}, reqEditorRole)
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
})
// error test
r.Get("/metrics/error", wrap(GenerateError))

View File

@@ -4,11 +4,10 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
func GetApiKeys(c *middleware.Context) Response {
func GetApiKeys(c *m.ReqContext) Response {
query := m.GetApiKeysQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
@@ -27,7 +26,7 @@ func GetApiKeys(c *middleware.Context) Response {
return Json(200, result)
}
func DeleteApiKey(c *middleware.Context) Response {
func DeleteApiKey(c *m.ReqContext) Response {
id := c.ParamsInt64(":id")
cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
@@ -40,7 +39,7 @@ func DeleteApiKey(c *middleware.Context) Response {
return ApiSuccess("API key deleted")
}
func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response {
func AddApiKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response {
if !cmd.Role.IsValid() {
return ApiError(400, "Invalid role specified", nil)
}

View File

@@ -56,7 +56,7 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
}
func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler {
return func(c *middleware.Context) {
return func(c *m.ReqContext) {
path := c.Params("*")
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId)

View File

@@ -157,11 +157,11 @@ func NewCacheServer() *CacheServer {
func newNotFound() *Avatar {
avatar := &Avatar{notFound: true}
// load transparent png into buffer
path := filepath.Join(setting.StaticRootPath, "img", "transparent.png")
// load user_profile png into buffer
path := filepath.Join(setting.StaticRootPath, "img", "user_profile.png")
if data, err := ioutil.ReadFile(path); err != nil {
log.Error(3, "Failed to read transparent.png, %v", path)
log.Error(3, "Failed to read user_profile.png, %v", path)
} else {
avatar.data = bytes.NewBuffer(data)
}

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"net/http"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1"
)
@@ -19,7 +19,7 @@ var (
)
type Response interface {
WriteTo(ctx *middleware.Context)
WriteTo(ctx *m.ReqContext)
}
type NormalResponse struct {
@@ -32,7 +32,7 @@ type NormalResponse struct {
func wrap(action interface{}) macaron.Handler {
return func(c *middleware.Context) {
return func(c *m.ReqContext) {
var res Response
val, err := c.Invoke(action)
if err == nil && val != nil && len(val) > 0 {
@@ -45,7 +45,7 @@ func wrap(action interface{}) macaron.Handler {
}
}
func (r *NormalResponse) WriteTo(ctx *middleware.Context) {
func (r *NormalResponse) WriteTo(ctx *m.ReqContext) {
if r.err != nil {
ctx.Logger.Error(r.errMessage, "error", r.err)
}

105
pkg/api/common_test.go Normal file
View File

@@ -0,0 +1,105 @@
package api
import (
"net/http"
"net/http/httptest"
"path/filepath"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"gopkg.in/macaron.v1"
. "github.com/smartystreets/goconvey/convey"
)
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
loggedInUserScenarioWithRole(desc, "GET", url, url, m.ROLE_EDITOR, fn)
}
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role m.RoleType, 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
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
}
return nil
})
switch method {
case "GET":
sc.m.Get(routePattern, sc.defaultHandler)
case "DELETE":
sc.m.Delete(routePattern, sc.defaultHandler)
}
fn(sc)
})
}
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
So(err, ShouldBeNil)
sc.req = req
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
So(err, ShouldBeNil)
sc.req = req
return sc
}
type scenarioContext struct {
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
}
func (sc *scenarioContext) exec() {
sc.m.ServeHTTP(sc.resp, sc.req)
}
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *m.ReqContext) Response
func setupScenarioContext(url string) *scenarioContext {
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
return sc
}

View File

@@ -14,15 +14,15 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error) {
func isDashboardStarredByUser(c *m.ReqContext, dashId int64) (bool, error) {
if !c.IsSignedIn {
return false, nil
}
@@ -38,20 +38,19 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
func dashboardGuardianResponse(err error) Response {
if err != nil {
return ApiError(500, "Error while checking dashboard permissions", err)
} else {
return ApiError(403, "Access denied to this dashboard", nil)
}
return ApiError(403, "Access denied to this dashboard", nil)
}
func GetDashboard(c *middleware.Context) Response {
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
func GetDashboard(c *m.ReqContext) Response {
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
if rsp != nil {
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canView, err := guardian.CanView(); err != nil || !canView {
fmt.Printf("%v", err)
return dashboardGuardianResponse(err)
}
@@ -89,7 +88,8 @@ func GetDashboard(c *middleware.Context) Response {
HasAcl: dash.HasAcl,
IsFolder: dash.IsFolder,
FolderId: dash.FolderId,
FolderTitle: "Root",
Url: dash.GetUrl(),
FolderTitle: "General",
}
// lookup folder title
@@ -99,6 +99,7 @@ func GetDashboard(c *middleware.Context) Response {
return ApiError(500, "Dashboard folder could not be read", err)
}
meta.FolderTitle = query.Result.Title
meta.FolderUrl = query.Result.GetUrl()
}
// make sure db version is in sync with json model version
@@ -124,21 +125,39 @@ func getUserLogin(userId int64) string {
}
}
func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) {
query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) {
var query m.GetDashboardQuery
if len(uid) > 0 {
query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgId}
} else {
query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
}
if err := bus.Dispatch(&query); err != nil {
return nil, ApiError(404, "Dashboard not found", err)
}
return query.Result, nil
}
func DeleteDashboard(c *middleware.Context) Response {
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
func DeleteDashboard(c *m.ReqContext) Response {
query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to retrieve dashboards by slug", err)
}
if len(query.Result) > 1 {
return Json(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
}
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
if rsp != nil {
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -148,32 +167,42 @@ func DeleteDashboard(c *middleware.Context) Response {
return ApiError(500, "Failed to delete dashboard", err)
}
var resp = map[string]interface{}{"title": dash.Title}
return Json(200, resp)
return Json(200, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
})
}
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
func DeleteDashboardByUid(c *m.ReqContext) Response {
dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid"))
if rsp != nil {
return rsp
}
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete dashboard", err)
}
return Json(200, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
})
}
func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
cmd.OrgId = c.OrgId
cmd.UserId = c.UserId
dash := cmd.GetDashboardModel()
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
if dash.IsFolder && dash.FolderId > 0 {
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
}
// Check if Title is empty
if dash.Title == "" {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
}
if dash.Id == 0 {
limitReached, err := middleware.QuotaReached(c, "dashboard")
if dash.Id == 0 && dash.Uid == "" {
limitReached, err := quota.QuotaReached(c, "dashboard")
if err != nil {
return ApiError(500, "failed to get quota", err)
}
@@ -182,18 +211,31 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
}
}
dashItem := &dashboards.SaveDashboardItem{
dashItem := &dashboards.SaveDashboardDTO{
Dashboard: dash,
Message: cmd.Message,
OrgId: c.OrgId,
UserId: c.UserId,
User: c.SignedInUser,
Overwrite: cmd.Overwrite,
}
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
dashboard, err := dashboards.NewService().SaveDashboard(dashItem)
if err == m.ErrDashboardTitleEmpty {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
if err == m.ErrDashboardTitleEmpty ||
err == m.ErrDashboardWithSameNameAsFolder ||
err == m.ErrDashboardFolderWithSameNameAsDashboard ||
err == m.ErrDashboardTypeMismatch ||
err == m.ErrDashboardInvalidUid ||
err == m.ErrDashboardUidToLong ||
err == m.ErrDashboardWithSameUIDExists ||
err == m.ErrFolderNotFound ||
err == m.ErrDashboardFolderCannotHaveParent ||
err == m.ErrDashboardFolderNameExists {
return ApiError(400, err.Error(), nil)
}
if err == m.ErrDashboardUpdateAccessDenied {
return ApiError(403, err.Error(), err)
}
if err == m.ErrDashboardContainsInvalidAlertData {
@@ -201,7 +243,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
}
if err != nil {
if err == m.ErrDashboardWithSameNameExists {
if err == m.ErrDashboardWithSameNameInFolderExists {
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
}
if err == m.ErrDashboardVersionMismatch {
@@ -226,20 +268,28 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
}
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
return Json(200, util.DynMap{
"status": "success",
"slug": dashboard.Slug,
"version": dashboard.Version,
"id": dashboard.Id,
"uid": dashboard.Uid,
"url": dashboard.GetUrl(),
})
}
func GetHomeDashboard(c *middleware.Context) Response {
func GetHomeDashboard(c *m.ReqContext) Response {
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
if err := bus.Dispatch(&prefsQuery); err != nil {
return ApiError(500, "Failed to get preferences", err)
}
if prefsQuery.Result.HomeDashboardId != 0 {
slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
slugQuery := m.GetDashboardRefByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
err := bus.Dispatch(&slugQuery)
if err == nil {
dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
return Json(200, &dashRedirect)
} else {
log.Warn("Failed to get slug from database, %s", err.Error())
@@ -255,7 +305,7 @@ func GetHomeDashboard(c *middleware.Context) Response {
dash := dtos.DashboardFullWithMeta{}
dash.Meta.IsHome = true
dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
dash.Meta.FolderTitle = "Root"
dash.Meta.FolderTitle = "General"
jsonParser := json.NewDecoder(file)
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
@@ -288,10 +338,10 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
}
// GetDashboardVersions returns all dashboard versions as JSON
func GetDashboardVersions(c *middleware.Context) Response {
func GetDashboardVersions(c *m.ReqContext) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -327,10 +377,10 @@ func GetDashboardVersions(c *middleware.Context) Response {
}
// GetDashboardVersion returns the dashboard version with the given ID.
func GetDashboardVersion(c *middleware.Context) Response {
func GetDashboardVersion(c *m.ReqContext) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -359,7 +409,19 @@ func GetDashboardVersion(c *middleware.Context) Response {
}
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiffOptions) Response {
func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOptions) Response {
guardianBase := guardian.New(apiOptions.Base.DashboardId, c.OrgId, c.SignedInUser)
if canSave, err := guardianBase.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
if apiOptions.Base.DashboardId != apiOptions.New.DashboardId {
guardianNew := guardian.New(apiOptions.New.DashboardId, c.OrgId, c.SignedInUser)
if canSave, err := guardianNew.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
}
options := dashdiffs.Options{
OrgId: c.OrgId,
@@ -386,19 +448,19 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff
if options.DiffType == dashdiffs.DiffDelta {
return Respond(200, result.Delta).Header("Content-Type", "application/json")
} else {
return Respond(200, result.Delta).Header("Content-Type", "text/html")
}
return Respond(200, result.Delta).Header("Content-Type", "text/html")
}
// RestoreDashboardVersion restores a dashboard to the given version.
func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"))
func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
if rsp != nil {
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -416,12 +478,13 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
saveCmd.UserId = c.UserId
saveCmd.Dashboard = version.Data
saveCmd.Dashboard.Set("version", dash.Version)
saveCmd.Dashboard.Set("uid", dash.Uid)
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
return PostDashboard(c, saveCmd)
}
func GetDashboardTags(c *middleware.Context) {
func GetDashboardTags(c *m.ReqContext) {
query := m.GetDashboardTagsQuery{OrgId: c.OrgId}
err := bus.Dispatch(&query)
if err != nil {

View File

@@ -1,79 +0,0 @@
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetDashboardAclList(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
acl, err := guardian.GetAcl()
if err != nil {
return ApiError(500, "Failed to get dashboard acl", err)
}
return Json(200, acl)
}
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.UpdateDashboardAclCommand{}
cmd.DashboardId = dashId
for _, item := range apiCmd.Items {
cmd.Items = append(cmd.Items, &m.DashboardAcl{
OrgId: c.OrgId,
DashboardId: dashId,
UserId: item.UserId,
TeamId: item.TeamId,
Role: item.Role,
Permission: item.Permission,
Created: time.Now(),
Updated: time.Now(),
})
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
return ApiError(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
}
return ApiSuccess("Dashboard acl updated")
}
func DeleteDashboardAcl(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
aclId := c.ParamsInt64(":aclId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete permission for user", err)
}
return Json(200, "")
}

View File

@@ -1,174 +0,0 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardAclApiEndpoint(t *testing.T) {
Convey("Given a dashboard acl", t, func() {
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
{Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
}
dtoRes := transformDashboardAclsToDTOs(mockResult)
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = dtoRes
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
teamResp := []*m.Team{}
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = teamResp
return nil
})
Convey("When user is org admin", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 5)
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
})
})
Convey("When user is editor and has admin permission in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
Convey("Should be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is a member of a team in the ACL with admin permission", func() {
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
Convey("When user is editor and has edit permission in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is editor and not in the ACL", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
Convey("Should not be able to access ACL", func() {
sc.handlerFunc = GetDashboardAclList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) {
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW})
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
return nil
})
Convey("Should be not be able to delete permission", func() {
sc.handlerFunc = DeleteDashboardAcl
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
})
}
func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO {
dtos := make([]*m.DashboardAclInfoDTO, 0)
for _, acl := range acls {
dto := &m.DashboardAclInfoDTO{
Id: acl.Id,
OrgId: acl.OrgId,
DashboardId: acl.DashboardId,
Permission: acl.Permission,
UserId: acl.UserId,
TeamId: acl.TeamId,
}
dtos = append(dtos, dto)
}
return dtos
}

View File

@@ -0,0 +1,90 @@
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetDashboardPermissionList(c *m.ReqContext) Response {
dashId := c.ParamsInt64(":dashboardId")
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
if rsp != nil {
return rsp
}
g := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
acl, err := g.GetAcl()
if err != nil {
return ApiError(500, "Failed to get dashboard permissions", err)
}
for _, perm := range acl {
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
}
return Json(200, acl)
}
func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
dashId := c.ParamsInt64(":dashboardId")
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
if rsp != nil {
return rsp
}
g := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
cmd := m.UpdateDashboardAclCommand{}
cmd.DashboardId = dashId
for _, item := range apiCmd.Items {
cmd.Items = append(cmd.Items, &m.DashboardAcl{
OrgId: c.OrgId,
DashboardId: dashId,
UserId: item.UserId,
TeamId: item.TeamId,
Role: item.Role,
Permission: item.Permission,
Created: time.Now(),
Updated: time.Now(),
})
}
if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
if err != nil {
if err == guardian.ErrGuardianPermissionExists ||
err == guardian.ErrGuardianOverride {
return ApiError(400, err.Error(), err)
}
return ApiError(500, "Error while checking dashboard permissions", err)
}
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
return ApiError(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
}
return ApiSuccess("Dashboard permissions updated")
}

View File

@@ -0,0 +1,209 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardPermissionApiEndpoint(t *testing.T) {
Convey("Dashboard permissions test", t, func() {
Convey("Given dashboard not exists", func() {
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
return m.ErrDashboardNotFound
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
})
Convey("Given user has no admin permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("Given user has admin permissions and permissions to update", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: true,
GetAclValue: []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
},
})
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
callGetDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 5)
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("When trying to update permissions with duplicate permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
})
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("When trying to override inherited permissions with lower presedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
)
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
})
}
func callGetDashboardPermissions(sc *scenarioContext) {
sc.handlerFunc = GetDashboardPermissionList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func callUpdateDashboardPermissions(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func updateDashboardPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, 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.OrgId = TestOrgID
sc.context.UserId = TestUserID
return UpdateDashboardPermissions(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -6,13 +6,13 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func GetSharingOptions(c *middleware.Context) {
func GetSharingOptions(c *m.ReqContext) {
c.JSON(200, util.DynMap{
"externalSnapshotURL": setting.ExternalSnapshotUrl,
"externalSnapshotName": setting.ExternalSnapshotName,
@@ -20,7 +20,7 @@ func GetSharingOptions(c *middleware.Context) {
})
}
func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotCommand) {
if cmd.Name == "" {
cmd.Name = "Unnamed snapshot"
}
@@ -56,7 +56,8 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho
})
}
func GetDashboardSnapshot(c *middleware.Context) {
// GET /api/snapshots/:key
func GetDashboardSnapshot(c *m.ReqContext) {
key := c.Params(":key")
query := &m.GetDashboardSnapshotQuery{Key: key}
@@ -90,19 +91,44 @@ func GetDashboardSnapshot(c *middleware.Context) {
c.JSON(200, dto)
}
func DeleteDashboardSnapshot(c *middleware.Context) {
// GET /api/snapshots-delete/:key
func DeleteDashboardSnapshot(c *m.ReqContext) Response {
key := c.Params(":key")
query := &m.GetDashboardSnapshotQuery{DeleteKey: key}
err := bus.Dispatch(query)
if err != nil {
return ApiError(500, "Failed to get dashboard snapshot", err)
}
if query.Result == nil {
return ApiError(404, "Failed to get dashboard snapshot", nil)
}
dashboard := query.Result.Dashboard
dashboardId := dashboard.Get("id").MustInt64()
guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser)
canEdit, err := guardian.CanEdit()
if err != nil {
return ApiError(500, "Error while checking permissions for snapshot", err)
}
if !canEdit && query.Result.UserId != c.SignedInUser.UserId {
return ApiError(403, "Access denied to this snapshot", nil)
}
cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key}
if err := bus.Dispatch(cmd); err != nil {
c.JsonApiErr(500, "Failed to delete dashboard snapshot", err)
return
return ApiError(500, "Failed to delete dashboard snapshot", err)
}
c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
return Json(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
}
func SearchDashboardSnapshots(c *middleware.Context) Response {
// GET /api/dashboard/snapshots
func SearchDashboardSnapshots(c *m.ReqContext) Response {
query := c.Query("query")
limit := c.QueryInt("limit")
@@ -111,9 +137,10 @@ func SearchDashboardSnapshots(c *middleware.Context) Response {
}
searchQuery := m.GetDashboardSnapshotsQuery{
Name: query,
Limit: limit,
OrgId: c.OrgId,
Name: query,
Limit: limit,
OrgId: c.OrgId,
SignedInUser: c.SignedInUser,
}
err := bus.Dispatch(&searchQuery)

View File

@@ -0,0 +1,97 @@
package api
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardSnapshotApiEndpoint(t *testing.T) {
Convey("Given a single snapshot", t, func() {
jsonModel, _ := simplejson.NewJson([]byte(`{"id":100}`))
mockSnapshotResult := &m.DashboardSnapshot{
Id: 1,
Dashboard: jsonModel,
Expires: time.Now().Add(time.Duration(1000) * time.Second),
UserId: 999999,
}
bus.AddHandler("test", func(query *m.GetDashboardSnapshotQuery) error {
query.Result = mockSnapshotResult
return nil
})
bus.AddHandler("test", func(cmd *m.DeleteDashboardSnapshotCommand) error {
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
teamResp := []*m.Team{}
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = teamResp
return nil
})
Convey("When user has editor role and is not in the ACL", func() {
Convey("Should not be able to delete snapshot", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is editor and dashboard has default ACL", func() {
aclMockResp = []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
Convey("Should be able to delete a snapshot", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
})
})
})
Convey("When user is editor and is the creator of the snapshot", func() {
aclMockResp = []*m.DashboardAclInfoDTO{}
mockSnapshotResult.UserId = TestUserID
Convey("Should be able to delete a snapshot", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
})
})
})
})
}

View File

@@ -2,35 +2,24 @@ package api
import (
"encoding/json"
"path/filepath"
"fmt"
"testing"
macaron "gopkg.in/macaron.v1"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
type fakeDashboardRepo struct {
inserted []*dashboards.SaveDashboardItem
getDashboard []*m.Dashboard
}
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
repo.inserted = append(repo.inserted, json)
return json.Dashboard, nil
}
var fakeRepo *fakeDashboardRepo
// This tests three main scenarios.
// If a user has access to execute an action on a dashboard:
// 1. and the dashboard is in a folder which does not have an acl
// 2. and the dashboard is in a folder which does have an acl
// 3. Post dashboard response tests
func TestDashboardApiEndpoint(t *testing.T) {
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
@@ -39,8 +28,17 @@ func TestDashboardApiEndpoint(t *testing.T) {
fakeDash.FolderId = 1
fakeDash.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{fakeDash}
query.Result = dashboards
return nil
})
var getDashboardQueries []*m.GetDashboardQuery
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
getDashboardQueries = append(getDashboardQueries, query)
return nil
})
@@ -62,20 +60,20 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
cmd := m.SaveDashboardCommand{
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
"id": fakeDash.Id,
}),
}
// This tests two scenarios:
// 1. user is an org viewer
// 2. user is an org editor
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
@@ -83,9 +81,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -97,19 +122,18 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
@@ -117,9 +141,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -131,33 +182,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("When saving a dashboard folder in another folder", func() {
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
query.Result.IsFolder = true
return nil
})
invalidCmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
Convey("Should return an error", func() {
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
})
})
})
})
@@ -168,6 +192,12 @@ func TestDashboardApiEndpoint(t *testing.T) {
fakeDash.HasAcl = true
setting.ViewersCanEdit = false
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{fakeDash}
query.Result = dashboards
return nil
})
aclMockResp := []*m.DashboardAclInfoDTO{
{
DashboardId: 1,
@@ -181,8 +211,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
var getDashboardQueries []*m.GetDashboardQuery
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
getDashboardQueries = append(getDashboardQueries, query)
return nil
})
@@ -191,30 +224,59 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
cmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": fakeDash.Id,
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
// This tests six scenarios:
// 1. user is an org viewer AND has no permissions for this dashboard
// 2. user is an org editor AND has no permissions for this dashboard
// 3. user is an org viewer AND has been granted edit permission for the dashboard
// 4. user is an org viewer AND all viewers have edit permission for this dashboard
// 5. user is an org viewer AND has been granted an admin permission
// 6. user is an org editor AND has been granted a view permission
Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
role := m.ROLE_VIEWER
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -226,28 +288,53 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should be denied access", func() {
So(sc.resp.Code, ShouldEqual, 403)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -259,18 +346,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Viewer but has an edit permission", func() {
role := m.ROLE_VIEWER
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -278,9 +360,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
@@ -288,9 +374,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -302,11 +415,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is an Org Viewer and viewers can edit", func() {
@@ -314,7 +422,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
setting.ViewersCanEdit = true
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -322,9 +430,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeFalse)
@@ -332,9 +444,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeFalse)
So(dash.Meta.CanAdmin, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
})
@@ -342,7 +481,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
role := m.ROLE_VIEWER
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -350,9 +489,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
@@ -360,9 +503,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should be able to get dashboard with edit rights", func() {
So(dash.Meta.CanEdit, ShouldBeTrue)
So(dash.Meta.CanSave, ShouldBeTrue)
So(dash.Meta.CanAdmin, ShouldBeTrue)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -374,18 +544,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
Convey("When user is an Org Editor but has a view permission", func() {
role := m.ROLE_EDITOR
mockResult := []*m.DashboardAclInfoDTO{
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
@@ -393,18 +558,48 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
dash := GetDashboardShouldReturn200(sc)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
Convey("Should not be able to edit or save dashboard", func() {
So(dash.Meta.CanEdit, ShouldBeFalse)
So(dash.Meta.CanSave, ShouldBeFalse)
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by slug", func() {
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
})
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
@@ -416,18 +611,188 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
Convey("Given two dashboards with the same title in different folders", t, func() {
dashOne := m.NewDashboard("dash")
dashOne.Id = 2
dashOne.FolderId = 1
dashOne.HasAcl = false
dashTwo := m.NewDashboard("dash")
dashTwo.Id = 4
dashTwo.FolderId = 3
dashTwo.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{dashOne, dashTwo}
query.Result = dashboards
return nil
})
role := m.ROLE_EDITOR
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
CallDeleteDashboard(sc)
Convey("Should result in 412 Precondition failed", func() {
So(sc.resp.Code, ShouldEqual, 412)
result := sc.ToJson()
So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists")
So(result.Get("message").MustString(), ShouldEqual, m.ErrDashboardsWithSameSlugExists.Error())
})
})
})
Convey("Post dashboard response tests", t, func() {
// This tests that a valid request returns correct response
Convey("Given a correct request for creating a dashboard", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
UserId: 5,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
Overwrite: true,
FolderId: 3,
IsFolder: false,
Message: "msg",
}
mock := &dashboards.FakeDashboardService{
SaveDashboardResult: &m.Dashboard{
Id: 2,
Uid: "uid",
Title: "Dash",
Slug: "dash",
Version: 2,
},
}
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) {
CallPostDashboardShouldReturnSuccess(sc)
Convey("It should call dashboard service with correct data", func() {
dto := mock.SavedDashboards[0]
So(dto.OrgId, ShouldEqual, cmd.OrgId)
So(dto.User.UserId, ShouldEqual, cmd.UserId)
So(dto.Dashboard.FolderId, ShouldEqual, 3)
So(dto.Dashboard.Title, ShouldEqual, "Dash")
So(dto.Overwrite, ShouldBeTrue)
So(dto.Message, ShouldEqual, "msg")
})
Convey("It should return correct response data", func() {
result := sc.ToJson()
So(result.Get("status").MustString(), ShouldEqual, "success")
So(result.Get("id").MustInt64(), ShouldEqual, 2)
So(result.Get("uid").MustString(), ShouldEqual, "uid")
So(result.Get("slug").MustString(), ShouldEqual, "dash")
So(result.Get("url").MustString(), ShouldEqual, "/d/uid/dash")
})
})
})
// This tests that invalid requests returns expected error responses
Convey("Given incorrect requests for creating a dashboard", func() {
testCases := []struct {
SaveError error
ExpectedStatusCode int
}{
{SaveError: m.ErrDashboardNotFound, ExpectedStatusCode: 404},
{SaveError: m.ErrFolderNotFound, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardWithSameUIDExists, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardWithSameNameInFolderExists, ExpectedStatusCode: 412},
{SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412},
{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardWithSameNameAsFolder, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderNameExists, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
}
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "",
}),
}
for _, tc := range testCases {
mock := &dashboards.FakeDashboardService{
SaveDashboardError: tc.SaveError,
}
postDashboardScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, tc.ExpectedStatusCode)
})
}
})
})
Convey("Given two dashboards being compared", t, func() {
mockResult := []*m.DashboardAclInfoDTO{}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = mockResult
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{
Data: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash" + string(query.DashboardId),
}),
}
return nil
})
cmd := dtos.CalculateDiffOptions{
Base: dtos.CalculateDiffTarget{
DashboardId: 1,
Version: 1,
},
New: dtos.CalculateDiffTarget{
DashboardId: 2,
Version: 2,
},
DiffType: "basic",
}
Convey("when user does not have permission", func() {
role := m.ROLE_VIEWER
postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("when user does have permission", func() {
role := m.ROLE_ADMIN
postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
}
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
CallGetDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
@@ -438,6 +803,11 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta
return dash
}
func CallGetDashboard(sc *scenarioContext) {
sc.handlerFunc = GetDashboard
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func CallGetDashboardVersion(sc *scenarioContext) {
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{}
@@ -467,55 +837,75 @@ func CallDeleteDashboard(sc *scenarioContext) {
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func CallDeleteDashboardByUid(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
return nil
})
sc.handlerFunc = DeleteDashboardByUid
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func CallPostDashboard(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
return nil
})
bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
}
func postDashboardScenario(desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd m.SaveDashboardCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
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
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
return PostDashboard(c, cmd)
})
fakeRepo = &fakeDashboardRepo{}
dashboards.SetRepository(fakeRepo)
origNewDashboardService := dashboards.NewService
dashboards.MockDashboardService(mock)
sc.m.Post(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewService = origNewDashboardService
}()
fn(sc)
})
}
func postDiffScenario(desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role m.RoleType, 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.SignedInUser = &m.SignedInUser{
OrgId: TestOrgID,
UserId: TestUserID,
}
sc.context.OrgRole = role
return CalculateDashboardDiff(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func (sc *scenarioContext) ToJson() *simplejson.Json {
var result *simplejson.Json
err := json.NewDecoder(sc.resp.Body).Decode(&result)
So(err, ShouldBeNil)
return result
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
@@ -35,7 +34,7 @@ func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m
return query.Result, nil
}
func (hs *HttpServer) ProxyDataSourceRequest(c *middleware.Context) {
func (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"

View File

@@ -5,13 +5,12 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
)
func GetDataSources(c *middleware.Context) Response {
func GetDataSources(c *m.ReqContext) Response {
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
@@ -50,7 +49,7 @@ func GetDataSources(c *middleware.Context) Response {
return Json(200, &result)
}
func GetDataSourceById(c *middleware.Context) Response {
func GetDataSourceById(c *m.ReqContext) Response {
query := m.GetDataSourceByIdQuery{
Id: c.ParamsInt64(":id"),
OrgId: c.OrgId,
@@ -69,7 +68,7 @@ func GetDataSourceById(c *middleware.Context) Response {
return Json(200, &dtos)
}
func DeleteDataSourceById(c *middleware.Context) Response {
func DeleteDataSourceById(c *m.ReqContext) Response {
id := c.ParamsInt64(":id")
if id <= 0 {
@@ -95,7 +94,7 @@ func DeleteDataSourceById(c *middleware.Context) Response {
return ApiSuccess("Data source deleted")
}
func DeleteDataSourceByName(c *middleware.Context) Response {
func DeleteDataSourceByName(c *m.ReqContext) Response {
name := c.Params(":name")
if name == "" {
@@ -120,7 +119,7 @@ func DeleteDataSourceByName(c *middleware.Context) Response {
return ApiSuccess("Data source deleted")
}
func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) Response {
func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
@@ -140,7 +139,7 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) Response {
})
}
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
@@ -205,7 +204,7 @@ func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) {
}
// Get /api/datasources/name/:name
func GetDataSourceByName(c *middleware.Context) Response {
func GetDataSourceByName(c *m.ReqContext) Response {
query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
@@ -221,7 +220,7 @@ func GetDataSourceByName(c *middleware.Context) Response {
}
// Get /api/datasources/id/:name
func GetDataSourceIdByName(c *middleware.Context) Response {
func GetDataSourceIdByName(c *m.ReqContext) Response {
query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {

View File

@@ -2,17 +2,11 @@ package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/models"
macaron "gopkg.in/macaron.v1"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
. "github.com/smartystreets/goconvey/convey"
)
@@ -54,88 +48,3 @@ func TestDataSourcesProxy(t *testing.T) {
})
})
}
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn)
}
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
}
return nil
})
switch method {
case "GET":
sc.m.Get(routePattern, sc.defaultHandler)
case "DELETE":
sc.m.Delete(routePattern, sc.defaultHandler)
}
fn(sc)
})
}
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
So(err, ShouldBeNil)
sc.req = req
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
So(err, ShouldBeNil)
sc.req = req
return sc
}
type scenarioContext struct {
m *macaron.Macaron
context *middleware.Context
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
}
func (sc *scenarioContext) exec() {
sc.m.ServeHTTP(sc.resp, sc.req)
}
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *middleware.Context) Response

View File

@@ -19,7 +19,8 @@ type AlertRule struct {
EvalDate time.Time `json:"evalDate"`
EvalData *simplejson.Json `json:"evalData"`
ExecutionError string `json:"executionError"`
DashbboardUri string `json:"dashboardUri"`
Url string `json:"url"`
CanEdit bool `json:"canEdit"`
}
type AlertNotification struct {

View File

@@ -16,6 +16,7 @@ type DashboardMeta struct {
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Url string `json:"url"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
@@ -26,6 +27,7 @@ type DashboardMeta struct {
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
}
type DashboardFullWithMeta struct {

25
pkg/api/dtos/folder.go Normal file
View File

@@ -0,0 +1,25 @@
package dtos
import "time"
type Folder struct {
Id int64 `json:"id"`
Uid string `json:"uid"`
Title string `json:"title"`
Url string `json:"url"`
HasAcl bool `json:"hasAcl"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CreatedBy string `json:"createdBy"`
Created time.Time `json:"created"`
UpdatedBy string `json:"updatedBy"`
Updated time.Time `json:"updated"`
Version int `json:"version"`
}
type FolderSearchHit struct {
Id int64 `json:"id"`
Uid string `json:"uid"`
Title string `json:"title"`
}

146
pkg/api/folder.go Normal file
View File

@@ -0,0 +1,146 @@
package api
import (
"fmt"
"github.com/grafana/grafana/pkg/api/dtos"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
func GetFolders(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folders, err := s.GetFolders(c.QueryInt("limit"))
if err != nil {
return toFolderError(err)
}
result := make([]dtos.FolderSearchHit, 0)
for _, f := range folders {
result = append(result, dtos.FolderSearchHit{
Id: f.Id,
Uid: f.Uid,
Title: f.Title,
})
}
return Json(200, result)
}
func GetFolderByUid(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUid(c.Params(":uid"))
if err != nil {
return toFolderError(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
return Json(200, toFolderDto(g, folder))
}
func GetFolderById(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderById(c.ParamsInt64(":id"))
if err != nil {
return toFolderError(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
return Json(200, toFolderDto(g, folder))
}
func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
err := s.CreateFolder(&cmd)
if err != nil {
return toFolderError(err)
}
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
return Json(200, toFolderDto(g, cmd.Result))
}
func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
err := s.UpdateFolder(c.Params(":uid"), &cmd)
if err != nil {
return toFolderError(err)
}
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
return Json(200, toFolderDto(g, cmd.Result))
}
func DeleteFolder(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
f, err := s.DeleteFolder(c.Params(":uid"))
if err != nil {
return toFolderError(err)
}
return Json(200, util.DynMap{
"title": f.Title,
"message": fmt.Sprintf("Folder %s deleted", f.Title),
})
}
func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder {
canEdit, _ := g.CanEdit()
canSave, _ := g.CanSave()
canAdmin, _ := g.CanAdmin()
// Finding creator and last updater of the folder
updater, creator := "Anonymous", "Anonymous"
if folder.CreatedBy > 0 {
creator = getUserLogin(folder.CreatedBy)
}
if folder.UpdatedBy > 0 {
updater = getUserLogin(folder.UpdatedBy)
}
return dtos.Folder{
Id: folder.Id,
Uid: folder.Uid,
Title: folder.Title,
Url: folder.Url,
HasAcl: folder.HasAcl,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CreatedBy: creator,
Created: folder.Created,
UpdatedBy: updater,
Updated: folder.Updated,
Version: folder.Version,
}
}
func toFolderError(err error) Response {
if err == m.ErrFolderTitleEmpty ||
err == m.ErrFolderSameNameExists ||
err == m.ErrFolderWithSameUIDExists ||
err == m.ErrDashboardTypeMismatch ||
err == m.ErrDashboardInvalidUid ||
err == m.ErrDashboardUidToLong {
return ApiError(400, err.Error(), nil)
}
if err == m.ErrFolderAccessDenied {
return ApiError(403, "Access denied", err)
}
if err == m.ErrFolderNotFound {
return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
}
if err == m.ErrFolderVersionMismatch {
return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
}
return ApiError(500, "Folder API error", err)
}

View File

@@ -0,0 +1,107 @@
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetFolderPermissionList(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUid(c.Params(":uid"))
if err != nil {
return toFolderError(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return toFolderError(m.ErrFolderAccessDenied)
}
acl, err := g.GetAcl()
if err != nil {
return ApiError(500, "Failed to get folder permissions", err)
}
for _, perm := range acl {
perm.FolderId = folder.Id
perm.DashboardId = 0
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
}
return Json(200, acl)
}
func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUid(c.Params(":uid"))
if err != nil {
return toFolderError(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
canAdmin, err := g.CanAdmin()
if err != nil {
return toFolderError(err)
}
if !canAdmin {
return toFolderError(m.ErrFolderAccessDenied)
}
cmd := m.UpdateDashboardAclCommand{}
cmd.DashboardId = folder.Id
for _, item := range apiCmd.Items {
cmd.Items = append(cmd.Items, &m.DashboardAcl{
OrgId: c.OrgId,
DashboardId: folder.Id,
UserId: item.UserId,
TeamId: item.TeamId,
Role: item.Role,
Permission: item.Permission,
Created: time.Now(),
Updated: time.Now(),
})
}
if okToUpdate, err := g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate {
if err != nil {
if err == guardian.ErrGuardianPermissionExists ||
err == guardian.ErrGuardianOverride {
return ApiError(400, err.Error(), err)
}
return ApiError(500, "Error while checking folder permissions", err)
}
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDashboardAclInfoMissing {
err = m.ErrFolderAclInfoMissing
}
if err == m.ErrDashboardPermissionDashboardEmpty {
err = m.ErrFolderPermissionFolderEmpty
}
if err == m.ErrFolderAclInfoMissing || err == m.ErrFolderPermissionFolderEmpty {
return ApiError(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
}
return ApiSuccess("Folder permissions updated")
}

View File

@@ -0,0 +1,241 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
. "github.com/smartystreets/goconvey/convey"
)
func TestFolderPermissionApiEndpoint(t *testing.T) {
Convey("Folder permissions test", t, func() {
Convey("Given folder not exists", func() {
mock := &fakeFolderService{
GetFolderByUidError: m.ErrFolderNotFound,
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
Reset(func() {
dashboards.NewFolderService = origNewFolderService
})
})
Convey("Given user has no admin permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
Convey("Given user has admin permissions and permissions to update", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: true,
GetAclValue: []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
},
})
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
callGetFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 5)
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
Convey("When trying to update permissions with duplicate permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
})
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
Convey("When trying to override inherited permissions with lower presedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
)
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
})
}
func callGetFolderPermissions(sc *scenarioContext) {
sc.handlerFunc = GetFolderPermissionList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func callUpdateFolderPermissions(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func updateFolderPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, 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.OrgId = TestOrgID
sc.context.UserId = TestUserID
return UpdateFolderPermissions(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}

251
pkg/api/folder_test.go Normal file
View File

@@ -0,0 +1,251 @@
package api
import (
"encoding/json"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
. "github.com/smartystreets/goconvey/convey"
)
func TestFoldersApiEndpoint(t *testing.T) {
Convey("Create/update folder response tests", t, func() {
Convey("Given a correct request for creating a folder", func() {
cmd := m.CreateFolderCommand{
Uid: "uid",
Title: "Folder",
}
mock := &fakeFolderService{
CreateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder"},
}
createFolderScenario("When calling POST on", "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
Convey("It should return correct response data", func() {
folder := dtos.Folder{}
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
So(err, ShouldBeNil)
So(folder.Id, ShouldEqual, 1)
So(folder.Uid, ShouldEqual, "uid")
So(folder.Title, ShouldEqual, "Folder")
})
})
})
Convey("Given incorrect requests for creating a folder", func() {
testCases := []struct {
Error error
ExpectedStatusCode int
}{
{Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400},
{Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
{Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400},
{Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403},
{Error: m.ErrFolderNotFound, ExpectedStatusCode: 404},
{Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
{Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500},
}
cmd := m.CreateFolderCommand{
Uid: "uid",
Title: "Folder",
}
for _, tc := range testCases {
mock := &fakeFolderService{
CreateFolderError: tc.Error,
}
createFolderScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()), "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
if sc.resp.Code != tc.ExpectedStatusCode {
t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code)
}
})
}
})
Convey("Given a correct request for updating a folder", func() {
cmd := m.UpdateFolderCommand{
Title: "Folder upd",
}
mock := &fakeFolderService{
UpdateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder upd"},
}
updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
callUpdateFolder(sc)
Convey("It should return correct response data", func() {
folder := dtos.Folder{}
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
So(err, ShouldBeNil)
So(folder.Id, ShouldEqual, 1)
So(folder.Uid, ShouldEqual, "uid")
So(folder.Title, ShouldEqual, "Folder upd")
})
})
})
Convey("Given incorrect requests for updating a folder", func() {
testCases := []struct {
Error error
ExpectedStatusCode int
}{
{Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400},
{Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
{Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400},
{Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403},
{Error: m.ErrFolderNotFound, ExpectedStatusCode: 404},
{Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
{Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500},
}
cmd := m.UpdateFolderCommand{
Title: "Folder upd",
}
for _, tc := range testCases {
mock := &fakeFolderService{
UpdateFolderError: tc.Error,
}
updateFolderScenario(fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()), "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
callUpdateFolder(sc)
if sc.resp.Code != tc.ExpectedStatusCode {
t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code)
}
})
}
})
})
}
func callGetFolderByUid(sc *scenarioContext) {
sc.handlerFunc = GetFolderByUid
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func callDeleteFolder(sc *scenarioContext) {
sc.handlerFunc = DeleteFolder
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func callCreateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func createFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.CreateFolderCommand, 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.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
return CreateFolder(c, cmd)
})
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
sc.m.Post(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewFolderService = origNewFolderService
}()
fn(sc)
})
}
func callUpdateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
}
func updateFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.UpdateFolderCommand, 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.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
return UpdateFolder(c, cmd)
})
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
sc.m.Put(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewFolderService = origNewFolderService
}()
fn(sc)
})
}
type fakeFolderService struct {
GetFoldersResult []*m.Folder
GetFoldersError error
GetFolderByUidResult *m.Folder
GetFolderByUidError error
GetFolderByIdResult *m.Folder
GetFolderByIdError error
CreateFolderResult *m.Folder
CreateFolderError error
UpdateFolderResult *m.Folder
UpdateFolderError error
DeleteFolderResult *m.Folder
DeleteFolderError error
DeletedFolderUids []string
}
func (s *fakeFolderService) GetFolders(limit int) ([]*m.Folder, error) {
return s.GetFoldersResult, s.GetFoldersError
}
func (s *fakeFolderService) GetFolderById(id int64) (*m.Folder, error) {
return s.GetFolderByIdResult, s.GetFolderByIdError
}
func (s *fakeFolderService) GetFolderByUid(uid string) (*m.Folder, error) {
return s.GetFolderByUidResult, s.GetFolderByUidError
}
func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
cmd.Result = s.CreateFolderResult
return s.CreateFolderError
}
func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *m.UpdateFolderCommand) error {
cmd.Result = s.UpdateFolderResult
return s.UpdateFolderError
}
func (s *fakeFolderService) DeleteFolder(uid string) (*m.Folder, error) {
s.DeletedFolderUids = append(s.DeletedFolderUids, uid)
return s.DeleteFolderResult, s.DeleteFolderError
}
func mockFolderService(mock *fakeFolderService) {
dashboards.NewFolderService = func(orgId int64, user *m.SignedInUser) dashboards.FolderService {
return mock
}
}

View File

@@ -5,14 +5,13 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, error) {
func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
orgDataSources := make([]*m.DataSource, 0)
if c.OrgId != 0 {
@@ -180,7 +179,7 @@ func getPanelSort(id string) int {
return sort
}
func GetFrontendSettings(c *middleware.Context) {
func GetFrontendSettings(c *m.ReqContext) {
settings, err := getFrontendSettingsMap(c)
if err != nil {
c.JsonApiErr(400, "Failed to get frontend settings", err)

View File

@@ -7,7 +7,7 @@ import (
"net/url"
"time"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -41,7 +41,7 @@ func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
return &httputil.ReverseProxy{Director: director}
}
func ProxyGnetRequest(c *middleware.Context) {
func ProxyGnetRequest(c *m.ReqContext) {
proxyPath := c.Params("*")
proxy := ReverseProxyGnetReq(proxyPath)
proxy.Transport = grafanaComProxyTransport

View File

@@ -162,6 +162,10 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
hs.mapStatic(m, setting.StaticRootPath, "", "public")
hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
if setting.ImageUploadProvider == "local" {
hs.mapStatic(m, setting.ImagesDir, "", "/public/img/attachments")
}
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: path.Join(setting.StaticRootPath, "views"),
IndentJSON: macaron.Env != macaron.PROD,

View File

@@ -6,13 +6,12 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
settings, err := getFrontendSettingsMap(c)
if err != nil {
return nil, err
@@ -74,7 +73,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
if setting.DisableGravatar {
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/transparent.png"
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png"
}
if len(data.User.Name) == 0 {
@@ -102,8 +101,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
{Divider: true, HideFromTabs: true},
{Text: "Home", Id: "home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
{Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true},
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"},
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"},
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"},
@@ -261,7 +260,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if c.IsGrafanaAdmin {
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Divider: true, HideFromTabs: true,
Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
})
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Text: "Server Admin",
@@ -299,7 +298,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
return &data, nil
}
func Index(c *middleware.Context) {
func Index(c *m.ReqContext) {
if data, err := setIndexViewData(c); err != nil {
c.Handle(500, "Failed to get settings", err)
return
@@ -308,7 +307,7 @@ func Index(c *middleware.Context) {
}
}
func NotFoundHandler(c *middleware.Context) {
func NotFoundHandler(c *m.ReqContext) {
if c.IsApiRequest() {
c.JsonApiErr(404, "Not found", nil)
return

View File

@@ -8,8 +8,8 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
)
@@ -17,7 +17,7 @@ const (
VIEW_INDEX = "index"
)
func LoginView(c *middleware.Context) {
func LoginView(c *m.ReqContext) {
viewData, err := setIndexViewData(c)
if err != nil {
c.Handle(500, "Failed to get settings", err)
@@ -53,7 +53,7 @@ func LoginView(c *middleware.Context) {
c.Redirect(setting.AppSubUrl + "/")
}
func tryLoginUsingRememberCookie(c *middleware.Context) bool {
func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
// Check auto-login.
uname := c.GetCookie(setting.CookieUserName)
if len(uname) == 0 {
@@ -87,7 +87,7 @@ func tryLoginUsingRememberCookie(c *middleware.Context) bool {
return true
}
func LoginApiPing(c *middleware.Context) {
func LoginApiPing(c *m.ReqContext) {
if !tryLoginUsingRememberCookie(c) {
c.JsonApiErr(401, "Unauthorized", nil)
return
@@ -96,18 +96,19 @@ func LoginApiPing(c *middleware.Context) {
c.JsonOK("Logged in")
}
func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response {
func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
if setting.DisableLoginForm {
return ApiError(401, "Login is disabled", nil)
}
authQuery := login.LoginUserQuery{
Username: cmd.User,
Password: cmd.Password,
Username: cmd.User,
Password: cmd.Password,
IpAddress: c.Req.RemoteAddr,
}
if err := bus.Dispatch(&authQuery); err != nil {
if err == login.ErrInvalidCredentials {
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
return ApiError(401, "Invalid username or password", err)
}
@@ -132,7 +133,7 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response {
return Json(200, result)
}
func loginUserWithUser(user *m.User, c *middleware.Context) {
func loginUserWithUser(user *m.User, c *m.ReqContext) {
if user == nil {
log.Error(3, "User login with nil user")
}
@@ -145,13 +146,13 @@ func loginUserWithUser(user *m.User, c *middleware.Context) {
c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/")
}
c.Session.RegenerateId(c)
c.Session.Set(middleware.SESS_KEY_USERID, user.Id)
c.Session.RegenerateId(c.Context)
c.Session.Set(session.SESS_KEY_USERID, user.Id)
}
func Logout(c *middleware.Context) {
func Logout(c *m.ReqContext) {
c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
c.Session.Destory(c)
c.Session.Destory(c.Context)
c.Redirect(setting.AppSubUrl + "/login")
}

View File

@@ -1,6 +1,7 @@
package api
import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
@@ -11,14 +12,14 @@ import (
"net/http"
"net/url"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
)
@@ -29,25 +30,25 @@ var (
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.login")
oauthLogger = log.New("oauth")
)
func GenStateString() string {
rnd := make([]byte, 32)
rand.Read(rnd)
return base64.StdEncoding.EncodeToString(rnd)
return base64.URLEncoding.EncodeToString(rnd)
}
func OAuthLogin(ctx *middleware.Context) {
func OAuthLogin(ctx *m.ReqContext) {
if setting.OAuthService == nil {
ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil)
ctx.Handle(404, "OAuth not enabled", nil)
return
}
name := ctx.Params(":name")
connect, ok := social.SocialMap[name]
if !ok {
ctx.Handle(404, "login.OAuthLogin(social login not enabled)", errors.New(name))
ctx.Handle(404, fmt.Sprintf("No OAuth with name %s configured", name), nil)
return
}
@@ -62,7 +63,7 @@ func OAuthLogin(ctx *middleware.Context) {
code := ctx.Query("code")
if code == "" {
state := GenStateString()
ctx.Session.Set(middleware.SESS_KEY_OAUTH_STATE, state)
ctx.Session.Set(session.SESS_KEY_OAUTH_STATE, state)
if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
} else {
@@ -71,7 +72,7 @@ func OAuthLogin(ctx *middleware.Context) {
return
}
savedState, ok := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string)
savedState, ok := ctx.Session.Get(session.SESS_KEY_OAUTH_STATE).(string)
if !ok {
ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
return
@@ -96,7 +97,9 @@ func OAuthLogin(ctx *middleware.Context) {
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" {
cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
if err != nil {
log.Fatal(1, "Failed to setup TlsClientCert", "oauth provider", name, "error", err)
ctx.Logger.Error("Failed to setup TlsClientCert", "oauth", name, "error", err)
ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCert)", nil)
return
}
tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
@@ -105,7 +108,9 @@ func OAuthLogin(ctx *middleware.Context) {
if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
if err != nil {
log.Fatal(1, "Failed to setup TlsClientCa", "oauth provider", name, "error", err)
ctx.Logger.Error("Failed to setup TlsClientCa", "oauth", name, "error", err)
ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCa)", nil)
return
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
@@ -124,13 +129,13 @@ func OAuthLogin(ctx *middleware.Context) {
// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
token.TokenType = "Bearer"
ctx.Logger.Debug("OAuthLogin Got token")
oauthLogger.Debug("OAuthLogin Got token", "token", token)
// set up oauth2 client
client := connect.Client(oauthCtx, token)
// get user info
userInfo, err := connect.UserInfo(client)
userInfo, err := connect.UserInfo(client, token)
if err != nil {
if sErr, ok := err.(*social.Error); ok {
redirectWithError(ctx, sErr)
@@ -140,7 +145,7 @@ func OAuthLogin(ctx *middleware.Context) {
return
}
ctx.Logger.Debug("OAuthLogin got user info", "userInfo", userInfo)
oauthLogger.Debug("OAuthLogin got user info", "userInfo", userInfo)
// validate that we got at least an email address
if userInfo.Email == "" {
@@ -163,7 +168,7 @@ func OAuthLogin(ctx *middleware.Context) {
redirectWithError(ctx, ErrSignUpNotAllowed)
return
}
limitReached, err := middleware.QuotaReached(ctx, "user")
limitReached, err := quota.QuotaReached(ctx, "user")
if err != nil {
ctx.Handle(500, "Failed to get user quota", err)
return
@@ -204,9 +209,8 @@ func OAuthLogin(ctx *middleware.Context) {
ctx.Redirect(setting.AppSubUrl + "/")
}
func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) {
ctx.Logger.Info(err.Error(), v...)
// TODO: we can use the flash storage here once it's implemented
func redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
ctx.Logger.Error(err.Error(), v...)
ctx.Session.Set("loginError", err.Error())
ctx.Redirect(setting.AppSubUrl + "/login")
}

View File

@@ -6,15 +6,14 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/tsdb/testdata"
"github.com/grafana/grafana/pkg/util"
)
// POST /api/tsdb/query
func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
if len(reqDto.Queries) == 0 {
@@ -26,7 +25,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
return ApiError(400, "Query missing datasourceId", nil)
}
dsQuery := models.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId}
dsQuery := m.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId}
if err := bus.Dispatch(&dsQuery); err != nil {
return ApiError(500, "failed to fetch data source", err)
}
@@ -61,7 +60,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
}
// GET /api/tsdb/testdata/scenarios
func GetTestDataScenarios(c *middleware.Context) Response {
func GetTestDataScenarios(c *m.ReqContext) Response {
result := make([]interface{}, 0)
for _, scenario := range testdata.ScenarioRegistry {
@@ -77,14 +76,14 @@ func GetTestDataScenarios(c *middleware.Context) Response {
}
// Genereates a index out of range error
func GenerateError(c *middleware.Context) Response {
func GenerateError(c *m.ReqContext) Response {
var array []string
return Json(200, array[20])
}
// GET /api/tsdb/testdata/gensql
func GenerateSqlTestData(c *middleware.Context) Response {
if err := bus.Dispatch(&models.InsertSqlTestDataCommand{}); err != nil {
func GenerateSqlTestData(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil {
return ApiError(500, "Failed to insert test data", err)
}
@@ -92,7 +91,7 @@ func GenerateSqlTestData(c *middleware.Context) Response {
}
// GET /api/tsdb/testdata/random-walk
func GetTestDataRandomWalk(c *middleware.Context) Response {
func GetTestDataRandomWalk(c *m.ReqContext) Response {
from := c.Query("from")
to := c.Query("to")
intervalMs := c.QueryInt64("intervalMs")
@@ -100,7 +99,7 @@ func GetTestDataRandomWalk(c *middleware.Context) Response {
timeRange := tsdb.NewTimeRange(from, to)
request := &tsdb.TsdbQuery{TimeRange: timeRange}
dsInfo := &models.DataSource{Type: "grafana-testdata-datasource"}
dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"}
request.Queries = append(request.Queries, &tsdb.Query{
RefId: "A",
IntervalMs: intervalMs,

View File

@@ -4,24 +4,23 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
// GET /api/org
func GetOrgCurrent(c *middleware.Context) Response {
func GetOrgCurrent(c *m.ReqContext) Response {
return getOrgHelper(c.OrgId)
}
// GET /api/orgs/:orgId
func GetOrgById(c *middleware.Context) Response {
func GetOrgById(c *m.ReqContext) Response {
return getOrgHelper(c.ParamsInt64(":orgId"))
}
// Get /api/orgs/name/:name
func GetOrgByName(c *middleware.Context) Response {
func GetOrgByName(c *m.ReqContext) Response {
query := m.GetOrgByNameQuery{Name: c.Params(":name")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrOrgNotFound {
@@ -76,7 +75,7 @@ func getOrgHelper(orgId int64) Response {
}
// POST /api/orgs
func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response {
func CreateOrg(c *m.ReqContext, cmd m.CreateOrgCommand) Response {
if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) {
return ApiError(403, "Access denied", nil)
}
@@ -98,12 +97,12 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response {
}
// PUT /api/org
func UpdateOrgCurrent(c *middleware.Context, form dtos.UpdateOrgForm) Response {
func UpdateOrgCurrent(c *m.ReqContext, form dtos.UpdateOrgForm) Response {
return updateOrgHelper(form, c.OrgId)
}
// PUT /api/orgs/:orgId
func UpdateOrg(c *middleware.Context, form dtos.UpdateOrgForm) Response {
func UpdateOrg(c *m.ReqContext, form dtos.UpdateOrgForm) Response {
return updateOrgHelper(form, c.ParamsInt64(":orgId"))
}
@@ -120,12 +119,12 @@ func updateOrgHelper(form dtos.UpdateOrgForm, orgId int64) Response {
}
// PUT /api/org/address
func UpdateOrgAddressCurrent(c *middleware.Context, form dtos.UpdateOrgAddressForm) Response {
func UpdateOrgAddressCurrent(c *m.ReqContext, form dtos.UpdateOrgAddressForm) Response {
return updateOrgAddressHelper(form, c.OrgId)
}
// PUT /api/orgs/:orgId/address
func UpdateOrgAddress(c *middleware.Context, form dtos.UpdateOrgAddressForm) Response {
func UpdateOrgAddress(c *m.ReqContext, form dtos.UpdateOrgAddressForm) Response {
return updateOrgAddressHelper(form, c.ParamsInt64(":orgId"))
}
@@ -150,7 +149,7 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Respons
}
// GET /api/orgs/:orgId
func DeleteOrgById(c *middleware.Context) Response {
func DeleteOrgById(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.DeleteOrgCommand{Id: c.ParamsInt64(":orgId")}); err != nil {
if err == m.ErrOrgNotFound {
return ApiError(404, "Failed to delete organization. ID not found", nil)
@@ -160,7 +159,7 @@ func DeleteOrgById(c *middleware.Context) Response {
return ApiSuccess("Organization deleted")
}
func SearchOrgs(c *middleware.Context) Response {
func SearchOrgs(c *m.ReqContext) Response {
query := m.SearchOrgsQuery{
Query: c.Query("query"),
Name: c.Query("name"),

View File

@@ -7,13 +7,12 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func GetPendingOrgInvites(c *middleware.Context) Response {
func GetPendingOrgInvites(c *m.ReqContext) Response {
query := m.GetTempUsersQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending}
if err := bus.Dispatch(&query); err != nil {
@@ -27,7 +26,7 @@ func GetPendingOrgInvites(c *middleware.Context) Response {
return Json(200, query.Result)
}
func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response {
func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
if !inviteDto.Role.IsValid() {
return ApiError(400, "Invalid role specified", nil)
}
@@ -89,7 +88,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
return ApiSuccess(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail))
}
func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dtos.AddInviteForm) Response {
func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddInviteForm) Response {
// user exists, add org role
createOrgUserCmd := m.AddOrgUserCommand{OrgId: c.OrgId, UserId: user.Id, Role: inviteDto.Role}
if err := bus.Dispatch(&createOrgUserCmd); err != nil {
@@ -119,7 +118,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto
}
}
func RevokeInvite(c *middleware.Context) Response {
func RevokeInvite(c *m.ReqContext) Response {
if ok, rsp := updateTempUserStatus(c.Params(":code"), m.TmpUserRevoked); !ok {
return rsp
}
@@ -127,7 +126,7 @@ func RevokeInvite(c *middleware.Context) Response {
return ApiSuccess("Invite revoked")
}
func GetInviteInfoByCode(c *middleware.Context) Response {
func GetInviteInfoByCode(c *m.ReqContext) Response {
query := m.GetTempUserByCodeQuery{Code: c.Params(":code")}
if err := bus.Dispatch(&query); err != nil {
@@ -147,7 +146,7 @@ func GetInviteInfoByCode(c *middleware.Context) Response {
})
}
func CompleteInvite(c *middleware.Context, completeInvite dtos.CompleteInviteForm) Response {
func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode}
if err := bus.Dispatch(&query); err != nil {

View File

@@ -3,18 +3,17 @@ package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
// POST /api/org/users
func AddOrgUserToCurrentOrg(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
func AddOrgUserToCurrentOrg(c *m.ReqContext, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.OrgId
return addOrgUserHelper(cmd)
}
// POST /api/orgs/:orgId/users
func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
func AddOrgUser(c *m.ReqContext, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
return addOrgUserHelper(cmd)
}
@@ -45,38 +44,42 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
}
// GET /api/org/users
func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
return getOrgUsersHelper(c.OrgId)
func GetOrgUsersForCurrentOrg(c *m.ReqContext) Response {
return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit"))
}
// GET /api/orgs/:orgId/users
func GetOrgUsers(c *middleware.Context) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId"))
func GetOrgUsers(c *m.ReqContext) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
}
func getOrgUsersHelper(orgId int64) Response {
query := m.GetOrgUsersQuery{OrgId: orgId}
func getOrgUsersHelper(orgId int64, query string, limit int) Response {
q := m.GetOrgUsersQuery{
OrgId: orgId,
Query: query,
Limit: limit,
}
if err := bus.Dispatch(&query); err != nil {
if err := bus.Dispatch(&q); err != nil {
return ApiError(500, "Failed to get account user", err)
}
for _, user := range query.Result {
for _, user := range q.Result {
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
}
return Json(200, query.Result)
return Json(200, q.Result)
}
// PATCH /api/org/users/:userId
func UpdateOrgUserForCurrentOrg(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
func UpdateOrgUserForCurrentOrg(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.OrgId
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
}
// PATCH /api/orgs/:orgId/users/:userId
func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
func UpdateOrgUser(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
@@ -98,13 +101,13 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
}
// DELETE /api/org/users/:userId
func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response {
func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response {
userId := c.ParamsInt64(":userId")
return removeOrgUserHelper(c.OrgId, userId)
}
// DELETE /api/orgs/:orgId/users/:userId
func RemoveOrgUser(c *middleware.Context) Response {
func RemoveOrgUser(c *m.ReqContext) Response {
userId := c.ParamsInt64(":userId")
orgId := c.ParamsInt64(":orgId")
return removeOrgUserHelper(orgId, userId)

View File

@@ -3,12 +3,11 @@ package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response {
func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailForm) Response {
userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
if err := bus.Dispatch(&userQuery); err != nil {
@@ -24,7 +23,7 @@ func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEm
return ApiSuccess("Email sent")
}
func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Response {
func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
query := m.ValidateResetPasswordCodeQuery{Code: form.Code}
if err := bus.Dispatch(&query); err != nil {

View File

@@ -3,11 +3,10 @@ package api
import (
"github.com/grafana/grafana/pkg/bus"
_ "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
func ValidateOrgPlaylist(c *middleware.Context) {
func ValidateOrgPlaylist(c *m.ReqContext) {
id := c.ParamsInt64(":id")
query := m.GetPlaylistByIdQuery{Id: id}
err := bus.Dispatch(&query)
@@ -40,7 +39,7 @@ func ValidateOrgPlaylist(c *middleware.Context) {
}
}
func SearchPlaylists(c *middleware.Context) Response {
func SearchPlaylists(c *m.ReqContext) Response {
query := c.Query("query")
limit := c.QueryInt("limit")
@@ -62,7 +61,7 @@ func SearchPlaylists(c *middleware.Context) Response {
return Json(200, searchQuery.Result)
}
func GetPlaylist(c *middleware.Context) Response {
func GetPlaylist(c *m.ReqContext) Response {
id := c.ParamsInt64(":id")
cmd := m.GetPlaylistByIdQuery{Id: id}
@@ -115,7 +114,7 @@ func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) {
return *itemQuery.Result, nil
}
func GetPlaylistItems(c *middleware.Context) Response {
func GetPlaylistItems(c *m.ReqContext) Response {
id := c.ParamsInt64(":id")
playlistDTOs, err := LoadPlaylistItemDTOs(id)
@@ -127,7 +126,7 @@ func GetPlaylistItems(c *middleware.Context) Response {
return Json(200, playlistDTOs)
}
func GetPlaylistDashboards(c *middleware.Context) Response {
func GetPlaylistDashboards(c *m.ReqContext) Response {
playlistId := c.ParamsInt64(":id")
playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId)
@@ -138,7 +137,7 @@ func GetPlaylistDashboards(c *middleware.Context) Response {
return Json(200, playlists)
}
func DeletePlaylist(c *middleware.Context) Response {
func DeletePlaylist(c *m.ReqContext) Response {
id := c.ParamsInt64(":id")
cmd := m.DeletePlaylistCommand{Id: id, OrgId: c.OrgId}
@@ -149,7 +148,7 @@ func DeletePlaylist(c *middleware.Context) Response {
return Json(200, "")
}
func CreatePlaylist(c *middleware.Context, cmd m.CreatePlaylistCommand) Response {
func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
@@ -159,7 +158,7 @@ func CreatePlaylist(c *middleware.Context, cmd m.CreatePlaylistCommand) Response
return Json(200, cmd.Result)
}
func UpdatePlaylist(c *middleware.Context, cmd m.UpdatePlaylistCommand) Response {
func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {

View File

@@ -18,7 +18,6 @@ import (
"github.com/opentracing/opentracing-go"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
@@ -42,14 +41,14 @@ type jwtToken struct {
type DataSourceProxy struct {
ds *m.DataSource
ctx *middleware.Context
ctx *m.ReqContext
targetUrl *url.URL
proxyPath string
route *plugins.AppPluginRoute
plugin *plugins.DataSourcePlugin
}
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *middleware.Context, proxyPath string) *DataSourceProxy {
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
targetUrl, _ := url.Parse(ds.Url)
return &DataSourceProxy{
@@ -190,8 +189,14 @@ func (proxy *DataSourceProxy) validateRequest() error {
}
if proxy.ds.Type == m.DS_PROMETHEUS {
if proxy.ctx.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxy.proxyPath, "api/") {
return errors.New("GET is only allowed on proxied Prometheus datasource")
if proxy.ctx.Req.Request.Method == "DELETE" {
return errors.New("Deletes not allowed on proxied Prometheus datasource")
}
if proxy.ctx.Req.Request.Method == "PUT" {
return errors.New("Puts not allowed on proxied Prometheus datasource")
}
if proxy.ctx.Req.Request.Method == "POST" && !(proxy.proxyPath == "api/v1/query" || proxy.proxyPath == "api/v1/query_range") {
return errors.New("Posts not allowed on proxied Prometheus datasource except on /query and /query_range")
}
}
@@ -255,7 +260,7 @@ func (proxy *DataSourceProxy) logRequest() {
"body", body)
}
func checkWhiteList(c *middleware.Context, host string) bool {
func checkWhiteList(c *m.ReqContext, host string) bool {
if host != "" && len(setting.DataProxyWhiteList) > 0 {
if _, exists := setting.DataProxyWhiteList[host]; !exists {
c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil)

View File

@@ -8,7 +8,6 @@ import (
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
@@ -61,7 +60,7 @@ func TestDSRouteRule(t *testing.T) {
}
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
ctx := &middleware.Context{
ctx := &m.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{Request: req},
},
@@ -104,7 +103,7 @@ func TestDSRouteRule(t *testing.T) {
Convey("When proxying graphite", func() {
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
ctx := &middleware.Context{}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
@@ -130,7 +129,7 @@ func TestDSRouteRule(t *testing.T) {
Password: "password",
}
ctx := &middleware.Context{}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
@@ -160,7 +159,7 @@ func TestDSRouteRule(t *testing.T) {
JsonData: json,
}
ctx := &middleware.Context{}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
@@ -186,7 +185,7 @@ func TestDSRouteRule(t *testing.T) {
JsonData: json,
}
ctx := &middleware.Context{}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")

View File

@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
@@ -38,7 +37,7 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.
return result, err
}
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
targetUrl, _ := url.Parse(route.Url)
director := func(req *http.Request) {

View File

@@ -5,13 +5,12 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
func GetPluginList(c *middleware.Context) Response {
func GetPluginList(c *m.ReqContext) Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
embeddedFilter := c.Query("embedded")
@@ -79,7 +78,7 @@ func GetPluginList(c *middleware.Context) Response {
return Json(200, result)
}
func GetPluginSettingById(c *middleware.Context) Response {
func GetPluginSettingById(c *m.ReqContext) Response {
pluginId := c.Params(":pluginId")
if def, exists := plugins.Plugins[pluginId]; !exists {
@@ -116,7 +115,7 @@ func GetPluginSettingById(c *middleware.Context) Response {
}
}
func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Response {
func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response {
pluginId := c.Params(":pluginId")
cmd.OrgId = c.OrgId
@@ -133,7 +132,7 @@ func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Re
return ApiSuccess("Plugin settings updated")
}
func GetPluginDashboards(c *middleware.Context) Response {
func GetPluginDashboards(c *m.ReqContext) Response {
pluginId := c.Params(":pluginId")
if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil {
@@ -147,7 +146,7 @@ func GetPluginDashboards(c *middleware.Context) Response {
}
}
func GetPluginMarkdown(c *middleware.Context) Response {
func GetPluginMarkdown(c *m.ReqContext) Response {
pluginId := c.Params(":pluginId")
name := c.Params(":name")
@@ -164,11 +163,11 @@ func GetPluginMarkdown(c *middleware.Context) Response {
}
}
func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Response {
func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response {
cmd := plugins.ImportDashboardCommand{
OrgId: c.OrgId,
UserId: c.UserId,
User: c.SignedInUser,
PluginId: apiCmd.PluginId,
Path: apiCmd.Path,
Inputs: apiCmd.Inputs,

View File

@@ -3,12 +3,11 @@ package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
// POST /api/preferences/set-home-dash
func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response {
func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response {
cmd.UserId = c.UserId
cmd.OrgId = c.OrgId
@@ -21,7 +20,7 @@ func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Respo
}
// GET /api/user/preferences
func GetUserPreferences(c *middleware.Context) Response {
func GetUserPreferences(c *m.ReqContext) Response {
return getPreferencesFor(c.OrgId, c.UserId)
}
@@ -42,7 +41,7 @@ func getPreferencesFor(orgId int64, userId int64) Response {
}
// PUT /api/user/preferences
func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response {
func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
}
@@ -63,11 +62,11 @@ func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd
}
// GET /api/org/preferences
func GetOrgPreferences(c *middleware.Context) Response {
func GetOrgPreferences(c *m.ReqContext) Response {
return getPreferencesFor(c.OrgId, 0)
}
// PUT /api/org/preferences
func UpdateOrgPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response {
func UpdateOrgPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
return updatePreferencesFor(c.OrgId, 0, &dtoCmd)
}

View File

@@ -2,12 +2,11 @@ package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func GetOrgQuotas(c *middleware.Context) Response {
func GetOrgQuotas(c *m.ReqContext) Response {
if !setting.Quota.Enabled {
return ApiError(404, "Quotas not enabled", nil)
}
@@ -20,7 +19,7 @@ func GetOrgQuotas(c *middleware.Context) Response {
return Json(200, query.Result)
}
func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateOrgQuotaCmd) Response {
func UpdateOrgQuota(c *m.ReqContext, cmd m.UpdateOrgQuotaCmd) Response {
if !setting.Quota.Enabled {
return ApiError(404, "Quotas not enabled", nil)
}
@@ -37,7 +36,7 @@ func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateOrgQuotaCmd) Response {
return ApiSuccess("Organization quota updated")
}
func GetUserQuotas(c *middleware.Context) Response {
func GetUserQuotas(c *m.ReqContext) Response {
if !setting.Quota.Enabled {
return ApiError(404, "Quotas not enabled", nil)
}
@@ -50,7 +49,7 @@ func GetUserQuotas(c *middleware.Context) Response {
return Json(200, query.Result)
}
func UpdateUserQuota(c *middleware.Context, cmd m.UpdateUserQuotaCmd) Response {
func UpdateUserQuota(c *m.ReqContext, cmd m.UpdateUserQuotaCmd) Response {
if !setting.Quota.Enabled {
return ApiError(404, "Quotas not enabled", nil)
}

View File

@@ -5,11 +5,11 @@ import (
"net/http"
"github.com/grafana/grafana/pkg/components/renderer"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func RenderToPng(c *middleware.Context) {
func RenderToPng(c *m.ReqContext) {
queryReader, err := util.NewUrlQueryReader(c.Req.URL)
if err != nil {
c.Handle(400, "Render parameters error", err)

View File

@@ -5,21 +5,26 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
)
func Search(c *middleware.Context) {
func Search(c *m.ReqContext) {
query := c.Query("query")
tags := c.QueryStrings("tag")
starred := c.Query("starred")
limit := c.QueryInt("limit")
dashboardType := c.Query("type")
permission := m.PERMISSION_VIEW
if limit == 0 {
limit = 1000
}
if c.Query("permission") == "Edit" {
permission = m.PERMISSION_EDIT
}
dbids := make([]int64, 0)
for _, id := range c.QueryStrings("dashboardIds") {
dashboardId, err := strconv.ParseInt(id, 10, 64)
@@ -46,6 +51,7 @@ func Search(c *middleware.Context) {
DashboardIds: dbids,
Type: dashboardType,
FolderIds: folderIds,
Permission: permission,
}
err := bus.Dispatch(&searchQuery)

View File

@@ -5,14 +5,13 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
// GET /api/user/signup/options
func GetSignUpOptions(c *middleware.Context) Response {
func GetSignUpOptions(c *m.ReqContext) Response {
return Json(200, util.DynMap{
"verifyEmailEnabled": setting.VerifyEmailEnabled,
"autoAssignOrg": setting.AutoAssignOrg,
@@ -20,7 +19,7 @@ func GetSignUpOptions(c *middleware.Context) Response {
}
// POST /api/user/signup
func SignUp(c *middleware.Context, form dtos.SignUpForm) Response {
func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
if !setting.AllowUserSignUp {
return ApiError(401, "User signup is disabled", nil)
}
@@ -52,7 +51,7 @@ func SignUp(c *middleware.Context, form dtos.SignUpForm) Response {
return Json(200, util.DynMap{"status": "SignUpCreated"})
}
func SignUpStep2(c *middleware.Context, form dtos.SignUpStep2Form) Response {
func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
if !setting.AllowUserSignUp {
return ApiError(401, "User signup is disabled", nil)
}

View File

@@ -2,11 +2,10 @@ package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
func StarDashboard(c *middleware.Context) Response {
func StarDashboard(c *m.ReqContext) Response {
if !c.IsSignedIn {
return ApiError(412, "You need to sign in to star dashboards", nil)
}
@@ -24,7 +23,7 @@ func StarDashboard(c *middleware.Context) Response {
return ApiSuccess("Dashboard starred!")
}
func UnstarDashboard(c *middleware.Context) Response {
func UnstarDashboard(c *m.ReqContext) Response {
cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}

View File

@@ -3,13 +3,12 @@ package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// POST /api/teams
func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
@@ -25,7 +24,8 @@ func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
}
// PUT /api/teams/:teamId
func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":teamId")
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
@@ -38,8 +38,8 @@ func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
}
// DELETE /api/teams/:teamId
func DeleteTeamById(c *middleware.Context) Response {
if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil {
func DeleteTeamById(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Failed to delete Team. ID not found", nil)
}
@@ -49,7 +49,7 @@ func DeleteTeamById(c *middleware.Context) Response {
}
// GET /api/teams/search
func SearchTeams(c *middleware.Context) Response {
func SearchTeams(c *m.ReqContext) Response {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
@@ -60,11 +60,11 @@ func SearchTeams(c *middleware.Context) Response {
}
query := m.SearchTeamsQuery{
OrgId: c.OrgId,
Query: c.Query("query"),
Name: c.Query("name"),
Page: page,
Limit: perPage,
OrgId: c.OrgId,
}
if err := bus.Dispatch(&query); err != nil {
@@ -82,8 +82,8 @@ func SearchTeams(c *middleware.Context) Response {
}
// GET /api/teams/:teamId
func GetTeamById(c *middleware.Context) Response {
query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")}
func GetTeamById(c *m.ReqContext) Response {
query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrTeamNotFound {

View File

@@ -3,14 +3,13 @@ package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
// GET /api/teams/:teamId/members
func GetTeamMembers(c *middleware.Context) Response {
query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")}
func GetTeamMembers(c *m.ReqContext) Response {
query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get Team Members", err)
@@ -24,14 +23,19 @@ func GetTeamMembers(c *middleware.Context) Response {
}
// POST /api/teams/:teamId/members
func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
cmd.TeamId = c.ParamsInt64(":teamId")
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamMemberAlreadyAdded {
return ApiError(400, "User is already added to this team", err)
if err == m.ErrTeamNotFound {
return ApiError(404, "Team not found", nil)
}
if err == m.ErrTeamMemberAlreadyAdded {
return ApiError(400, "User is already added to this team", nil)
}
return ApiError(500, "Failed to add Member to Team", err)
}
@@ -41,8 +45,16 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
}
// DELETE /api/teams/:teamId/members/:userId
func RemoveTeamMember(c *middleware.Context) Response {
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
func RemoveTeamMember(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Team not found", nil)
}
if err == m.ErrTeamMemberNotFound {
return ApiError(404, "Team member not found", nil)
}
return ApiError(500, "Failed to remove Member from Team", err)
}
return ApiSuccess("Team Member removed")

View File

@@ -3,19 +3,18 @@ package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
// GET /api/user (current authenticated user)
func GetSignedInUser(c *middleware.Context) Response {
func GetSignedInUser(c *m.ReqContext) Response {
return getUserUserProfile(c.UserId)
}
// GET /api/users/:id
func GetUserById(c *middleware.Context) Response {
func GetUserById(c *m.ReqContext) Response {
return getUserUserProfile(c.ParamsInt64(":id"))
}
@@ -33,7 +32,7 @@ func getUserUserProfile(userId int64) Response {
}
// GET /api/users/lookup
func GetUserByLoginOrEmail(c *middleware.Context) Response {
func GetUserByLoginOrEmail(c *m.ReqContext) Response {
query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrUserNotFound {
@@ -55,7 +54,7 @@ func GetUserByLoginOrEmail(c *middleware.Context) Response {
}
// POST /api/user
func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
func UpdateSignedInUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response {
if setting.AuthProxyEnabled {
if setting.AuthProxyHeaderProperty == "email" && cmd.Email != c.Email {
return ApiError(400, "Not allowed to change email when auth proxy is using email property", nil)
@@ -69,13 +68,13 @@ func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response
}
// POST /api/users/:id
func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
func UpdateUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response {
cmd.UserId = c.ParamsInt64(":id")
return handleUpdateUser(cmd)
}
//POST /api/users/:id/using/:orgId
func UpdateUserActiveOrg(c *middleware.Context) Response {
func UpdateUserActiveOrg(c *m.ReqContext) Response {
userId := c.ParamsInt64(":id")
orgId := c.ParamsInt64(":orgId")
@@ -108,12 +107,12 @@ func handleUpdateUser(cmd m.UpdateUserCommand) Response {
}
// GET /api/user/orgs
func GetSignedInUserOrgList(c *middleware.Context) Response {
func GetSignedInUserOrgList(c *m.ReqContext) Response {
return getUserOrgList(c.UserId)
}
// GET /api/user/:id/orgs
func GetUserOrgList(c *middleware.Context) Response {
func GetUserOrgList(c *m.ReqContext) Response {
return getUserOrgList(c.ParamsInt64(":id"))
}
@@ -146,7 +145,7 @@ func validateUsingOrg(userId int64, orgId int64) bool {
}
// POST /api/user/using/:id
func UserSetUsingOrg(c *middleware.Context) Response {
func UserSetUsingOrg(c *m.ReqContext) Response {
orgId := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) {
@@ -163,7 +162,7 @@ func UserSetUsingOrg(c *middleware.Context) Response {
}
// GET /profile/switch-org/:id
func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) {
func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
orgId := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) {
@@ -179,7 +178,7 @@ func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) {
c.Redirect(setting.AppSubUrl + "/")
}
func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response {
func ChangeUserPassword(c *m.ReqContext, cmd m.ChangeUserPasswordCommand) Response {
if setting.LdapEnabled || setting.AuthProxyEnabled {
return ApiError(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil)
}
@@ -211,7 +210,7 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
}
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
func SearchUsers(c *m.ReqContext) Response {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
@@ -221,7 +220,7 @@ func SearchUsers(c *middleware.Context) Response {
}
// GET /api/users/search
func SearchUsersWithPaging(c *middleware.Context) Response {
func SearchUsersWithPaging(c *m.ReqContext) Response {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
@@ -230,7 +229,7 @@ func SearchUsersWithPaging(c *middleware.Context) Response {
return Json(200, query.Result)
}
func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
func searchUser(c *m.ReqContext) (*m.SearchUsersQuery, error) {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
@@ -258,7 +257,7 @@ func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
return query, nil
}
func SetHelpFlag(c *middleware.Context) Response {
func SetHelpFlag(c *m.ReqContext) Response {
flag := c.ParamsInt64(":id")
bitmask := &c.HelpFlags1
@@ -276,7 +275,7 @@ func SetHelpFlag(c *middleware.Context) Response {
return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
}
func ClearHelpFlags(c *middleware.Context) Response {
func ClearHelpFlags(c *m.ReqContext) Response {
cmd := m.SetUserHelpFlagCommand{
UserId: c.UserId,
HelpFlags1: m.HelpFlags1(0),

View File

@@ -94,7 +94,7 @@ func InstallPlugin(pluginName, version string, c CommandLine) error {
res, _ := s.ReadPlugin(pluginFolder, pluginName)
for _, v := range res.Dependencies.Plugins {
InstallPlugin(v.Id, version, c)
InstallPlugin(v.Id, "", c)
logger.Infof("Installed dependency: %v ✔\n", v.Id)
}

View File

@@ -62,17 +62,22 @@ func (g *GrafanaServerImpl) Start() error {
search.Init()
login.Init()
social.NewOAuthService()
plugins.Init()
pluginManager, err := plugins.NewPluginManager(g.context)
if err != nil {
return fmt.Errorf("Failed to start plugins. error: %v", err)
}
g.childRoutines.Go(func() error { return pluginManager.Run(g.context) })
if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil {
return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
}
closer, err := tracing.Init(setting.Cfg)
tracingCloser, err := tracing.Init(setting.Cfg)
if err != nil {
return fmt.Errorf("Tracing settings is not valid. error: %v", err)
}
defer closer.Close()
defer tracingCloser.Close()
// init alerting
if setting.AlertingEnabled && setting.ExecuteAlerts {

View File

@@ -88,6 +88,8 @@ func NewImageUploader() (ImageUploader, error) {
container_name := azureBlobSec.Key("container_name").MustString("")
return NewAzureBlobUploader(account_name, account_key, container_name), nil
case "local":
return NewLocalImageUploader()
}
if setting.ImageUploadProvider != "" {

View File

@@ -143,5 +143,23 @@ func TestImageUploaderFactory(t *testing.T) {
So(original.container_name, ShouldEqual, "container_name")
})
})
Convey("Local uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "local"
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*LocalUploader)
So(ok, ShouldBeTrue)
So(original, ShouldNotBeNil)
})
})
}

View File

@@ -0,0 +1,22 @@
package imguploader
import (
"context"
"path"
"path/filepath"
"github.com/grafana/grafana/pkg/setting"
)
type LocalUploader struct {
}
func (u *LocalUploader) Upload(ctx context.Context, imageOnDiskPath string) (string, error) {
filename := filepath.Base(imageOnDiskPath)
image_url := setting.ToAbsUrl(path.Join("public/img/attachments", filename))
return image_url, nil
}
func NewLocalImageUploader() (*LocalUploader, error) {
return &LocalUploader{}, nil
}

View File

@@ -0,0 +1,18 @@
package imguploader
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestUploadToLocal(t *testing.T) {
Convey("[Integration test] for external_image_store.local", t, func() {
localUploader, _ := NewLocalImageUploader()
path, err := localUploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
So(err, ShouldBeNil)
So(path, ShouldContainSubstring, "/public/img/attachments")
})
}

View File

@@ -91,9 +91,15 @@ func RenderToPng(params *RenderOpts) (string, error) {
timeout = 15
}
phantomDebugArg := "--debug=false"
if log.GetLogLevelFor("png-renderer") >= log.LvlDebug {
phantomDebugArg = "--debug=true"
}
cmdArgs := []string{
"--ignore-ssl-errors=true",
"--web-security=false",
phantomDebugArg,
scriptPath,
"url=" + url,
"width=" + params.Width,
@@ -109,15 +115,13 @@ func RenderToPng(params *RenderOpts) (string, error) {
}
cmd := exec.Command(binPath, cmdArgs...)
stdout, err := cmd.StdoutPipe()
output, err := cmd.StdoutPipe()
if err != nil {
rendererLog.Error("Could not acquire stdout pipe", err)
return "", err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
cmd.Stderr = cmd.Stdout
if params.Timezone != "" {
baseEnviron := os.Environ()
@@ -126,11 +130,12 @@ func RenderToPng(params *RenderOpts) (string, error) {
err = cmd.Start()
if err != nil {
rendererLog.Error("Could not start command", err)
return "", err
}
go io.Copy(os.Stdout, stdout)
go io.Copy(os.Stdout, stderr)
logWriter := log.NewLogWriter(rendererLog, log.LvlDebug, "[phantom] ")
go io.Copy(logWriter, output)
done := make(chan error)
go func() {

View File

@@ -21,6 +21,7 @@ import (
var Root log15.Logger
var loggersToClose []DisposableHandler
var filters map[string]log15.Lvl
func init() {
loggersToClose = make([]DisposableHandler, 0)
@@ -114,6 +115,25 @@ func Close() {
loggersToClose = make([]DisposableHandler, 0)
}
func GetLogLevelFor(name string) Lvl {
if level, ok := filters[name]; ok {
switch level {
case log15.LvlWarn:
return LvlWarn
case log15.LvlInfo:
return LvlInfo
case log15.LvlError:
return LvlError
case log15.LvlCrit:
return LvlCrit
default:
return LvlDebug
}
}
return LvlInfo
}
var logLevels = map[string]log15.Lvl{
"trace": log15.LvlDebug,
"debug": log15.LvlDebug,
@@ -187,7 +207,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
// Log level.
_, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg)
modeFilters := getFilters(util.SplitString(sec.Key("filters").String()))
filters := getFilters(util.SplitString(sec.Key("filters").String()))
format := getLogFormat(sec.Key("format").MustString(""))
var handler log15.Handler
@@ -219,12 +239,12 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
}
for key, value := range defaultFilters {
if _, exist := modeFilters[key]; !exist {
modeFilters[key] = value
if _, exist := filters[key]; !exist {
filters[key] = value
}
}
handler = LogFilterHandler(level, modeFilters, handler)
handler = LogFilterHandler(level, filters, handler)
handlers = append(handlers, handler)
}
@@ -236,8 +256,8 @@ func LogFilterHandler(maxLevel log15.Lvl, filters map[string]log15.Lvl, h log15.
if len(filters) > 0 {
for i := 0; i < len(r.Ctx); i += 2 {
key := r.Ctx[i].(string)
if key == "logger" {
key, ok := r.Ctx[i].(string)
if ok && key == "logger" {
loggerName, strOk := r.Ctx[i+1].(string)
if strOk {
if filterLevel, ok := filters[loggerName]; ok {

39
pkg/log/log_writer.go Normal file
View File

@@ -0,0 +1,39 @@
package log
import (
"io"
"strings"
)
type logWriterImpl struct {
log Logger
level Lvl
prefix string
}
func NewLogWriter(log Logger, level Lvl, prefix string) io.Writer {
return &logWriterImpl{
log: log,
level: level,
prefix: prefix,
}
}
func (l *logWriterImpl) Write(p []byte) (n int, err error) {
message := l.prefix + strings.TrimSpace(string(p))
switch l.level {
case LvlCrit:
l.log.Crit(message)
case LvlError:
l.log.Error(message)
case LvlWarn:
l.log.Warn(message)
case LvlInfo:
l.log.Info(message)
default:
l.log.Debug(message)
}
return len(p), nil
}

116
pkg/log/log_writer_test.go Normal file
View File

@@ -0,0 +1,116 @@
package log
import (
"testing"
"github.com/inconshreveable/log15"
. "github.com/smartystreets/goconvey/convey"
)
type FakeLogger struct {
debug string
info string
warn string
err string
crit string
}
func (f *FakeLogger) New(ctx ...interface{}) log15.Logger {
return nil
}
func (f *FakeLogger) Debug(msg string, ctx ...interface{}) {
f.debug = msg
}
func (f *FakeLogger) Info(msg string, ctx ...interface{}) {
f.info = msg
}
func (f *FakeLogger) Warn(msg string, ctx ...interface{}) {
f.warn = msg
}
func (f *FakeLogger) Error(msg string, ctx ...interface{}) {
f.err = msg
}
func (f *FakeLogger) Crit(msg string, ctx ...interface{}) {
f.crit = msg
}
func (f *FakeLogger) GetHandler() log15.Handler {
return nil
}
func (f *FakeLogger) SetHandler(l log15.Handler) {}
func TestLogWriter(t *testing.T) {
Convey("When writing to a LogWriter", t, func() {
Convey("Should write using the correct level [crit]", func() {
fake := &FakeLogger{}
crit := NewLogWriter(fake, LvlCrit, "")
n, err := crit.Write([]byte("crit"))
So(n, ShouldEqual, 4)
So(err, ShouldBeNil)
So(fake.crit, ShouldEqual, "crit")
})
Convey("Should write using the correct level [error]", func() {
fake := &FakeLogger{}
crit := NewLogWriter(fake, LvlError, "")
n, err := crit.Write([]byte("error"))
So(n, ShouldEqual, 5)
So(err, ShouldBeNil)
So(fake.err, ShouldEqual, "error")
})
Convey("Should write using the correct level [warn]", func() {
fake := &FakeLogger{}
crit := NewLogWriter(fake, LvlWarn, "")
n, err := crit.Write([]byte("warn"))
So(n, ShouldEqual, 4)
So(err, ShouldBeNil)
So(fake.warn, ShouldEqual, "warn")
})
Convey("Should write using the correct level [info]", func() {
fake := &FakeLogger{}
crit := NewLogWriter(fake, LvlInfo, "")
n, err := crit.Write([]byte("info"))
So(n, ShouldEqual, 4)
So(err, ShouldBeNil)
So(fake.info, ShouldEqual, "info")
})
Convey("Should write using the correct level [debug]", func() {
fake := &FakeLogger{}
crit := NewLogWriter(fake, LvlDebug, "")
n, err := crit.Write([]byte("debug"))
So(n, ShouldEqual, 5)
So(err, ShouldBeNil)
So(fake.debug, ShouldEqual, "debug")
})
Convey("Should prefix the output with the prefix", func() {
fake := &FakeLogger{}
crit := NewLogWriter(fake, LvlDebug, "prefix")
n, err := crit.Write([]byte("debug"))
So(n, ShouldEqual, 5) // n is how much of input consumed
So(err, ShouldBeNil)
So(fake.debug, ShouldEqual, "prefixdebug")
})
})
}

View File

@@ -3,21 +3,20 @@ package login
import (
"errors"
"crypto/subtle"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
ErrInvalidCredentials = errors.New("Invalid Username or Password")
ErrInvalidCredentials = errors.New("Invalid Username or Password")
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
)
type LoginUserQuery struct {
Username string
Password string
User *m.User
Username string
Password string
User *m.User
IpAddress string
}
func Init() {
@@ -26,41 +25,31 @@ func Init() {
}
func AuthenticateUser(query *LoginUserQuery) error {
err := loginUsingGrafanaDB(query)
if err == nil || err != ErrInvalidCredentials {
if err := validateLoginAttempts(query.Username); err != nil {
return err
}
if setting.LdapEnabled {
for _, server := range LdapCfg.Servers {
author := NewLdapAuthenticator(server)
err = author.Login(query)
if err == nil || err != ErrInvalidCredentials {
return err
}
err := loginUsingGrafanaDB(query)
if err == nil || (err != m.ErrUserNotFound && err != ErrInvalidCredentials) {
return err
}
ldapEnabled, ldapErr := loginUsingLdap(query)
if ldapEnabled {
if ldapErr == nil || ldapErr != ErrInvalidCredentials {
return ldapErr
}
err = ldapErr
}
if err == ErrInvalidCredentials {
saveInvalidLoginAttempt(query)
}
if err == m.ErrUserNotFound {
return ErrInvalidCredentials
}
return err
}
func loginUsingGrafanaDB(query *LoginUserQuery) error {
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
if err := bus.Dispatch(&userQuery); err != nil {
if err == m.ErrUserNotFound {
return ErrInvalidCredentials
}
return err
}
user := userQuery.Result
passwordHashed := util.EncodePassword(query.Password, user.Salt)
if subtle.ConstantTimeCompare([]byte(passwordHashed), []byte(user.Password)) != 1 {
return ErrInvalidCredentials
}
query.User = user
return nil
}

214
pkg/login/auth_test.go Normal file
View File

@@ -0,0 +1,214 @@
package login
import (
"errors"
"testing"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAuthenticateUser(t *testing.T) {
Convey("Authenticate user", t, func() {
authScenario("When a user authenticates having too many login attempts", func(sc *authScenarioContext) {
mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc)
mockLoginUsingGrafanaDB(nil, sc)
mockLoginUsingLdap(true, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, ErrTooManyLoginAttempts)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeFalse)
So(sc.ldapLoginWasCalled, ShouldBeFalse)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
})
})
authScenario("When grafana user authenticate with valid credentials", func(sc *authScenarioContext) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(nil, sc)
mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, nil)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
So(sc.ldapLoginWasCalled, ShouldBeFalse)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
})
})
authScenario("When grafana user authenticate and unexpected error occurs", func(sc *authScenarioContext) {
customErr := errors.New("custom")
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(customErr, sc)
mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, customErr)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
So(sc.ldapLoginWasCalled, ShouldBeFalse)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
})
})
authScenario("When a non-existing grafana user authenticate and ldap disabled", func(sc *authScenarioContext) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
mockLoginUsingLdap(false, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
So(sc.ldapLoginWasCalled, ShouldBeTrue)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
})
})
authScenario("When a non-existing grafana user authenticate and invalid ldap credentials", func(sc *authScenarioContext) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
So(sc.ldapLoginWasCalled, ShouldBeTrue)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeTrue)
})
})
authScenario("When a non-existing grafana user authenticate and valid ldap credentials", func(sc *authScenarioContext) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
mockLoginUsingLdap(true, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldBeNil)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
So(sc.ldapLoginWasCalled, ShouldBeTrue)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
})
})
authScenario("When a non-existing grafana user authenticate and ldap returns unexpected error", func(sc *authScenarioContext) {
customErr := errors.New("custom")
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
mockLoginUsingLdap(true, customErr, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, customErr)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
So(sc.ldapLoginWasCalled, ShouldBeTrue)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
})
})
authScenario("When grafana user authenticate with invalid credentials and invalid ldap credentials", func(sc *authScenarioContext) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(ErrInvalidCredentials, sc)
mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
So(sc.grafanaLoginWasCalled, ShouldBeTrue)
So(sc.ldapLoginWasCalled, ShouldBeTrue)
So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeTrue)
})
})
})
}
type authScenarioContext struct {
loginUserQuery *LoginUserQuery
grafanaLoginWasCalled bool
ldapLoginWasCalled bool
loginAttemptValidationWasCalled bool
saveInvalidLoginAttemptWasCalled bool
}
type authScenarioFunc func(sc *authScenarioContext)
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
loginUsingGrafanaDB = func(query *LoginUserQuery) error {
sc.grafanaLoginWasCalled = true
return err
}
}
func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
sc.ldapLoginWasCalled = true
return enabled, err
}
}
func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
validateLoginAttempts = func(username string) error {
sc.loginAttemptValidationWasCalled = true
return err
}
}
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
saveInvalidLoginAttempt = func(query *LoginUserQuery) {
sc.saveInvalidLoginAttemptWasCalled = true
}
}
func authScenario(desc string, fn authScenarioFunc) {
Convey(desc, func() {
origLoginUsingGrafanaDB := loginUsingGrafanaDB
origLoginUsingLdap := loginUsingLdap
origValidateLoginAttempts := validateLoginAttempts
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
sc := &authScenarioContext{
loginUserQuery: &LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
},
}
defer func() {
loginUsingGrafanaDB = origLoginUsingGrafanaDB
loginUsingLdap = origLoginUsingLdap
validateLoginAttempts = origValidateLoginAttempts
saveInvalidLoginAttempt = origSaveInvalidLoginAttempt
}()
fn(sc)
})
}

View File

@@ -0,0 +1,48 @@
package login
import (
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
var (
maxInvalidLoginAttempts int64 = 5
loginAttemptsWindow time.Duration = time.Minute * 5
)
var validateLoginAttempts = func(username string) error {
if setting.DisableBruteForceLoginProtection {
return nil
}
loginAttemptCountQuery := m.GetUserLoginAttemptCountQuery{
Username: username,
Since: time.Now().Add(-loginAttemptsWindow),
}
if err := bus.Dispatch(&loginAttemptCountQuery); err != nil {
return err
}
if loginAttemptCountQuery.Result >= maxInvalidLoginAttempts {
return ErrTooManyLoginAttempts
}
return nil
}
var saveInvalidLoginAttempt = func(query *LoginUserQuery) {
if setting.DisableBruteForceLoginProtection {
return
}
loginAttemptCommand := m.CreateLoginAttemptCommand{
Username: query.Username,
IpAddress: query.IpAddress,
}
bus.Dispatch(&loginAttemptCommand)
}

View File

@@ -0,0 +1,125 @@
package login
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestLoginAttemptsValidation(t *testing.T) {
Convey("Validate login attempts", t, func() {
Convey("Given brute force login protection enabled", func() {
setting.DisableBruteForceLoginProtection = false
Convey("When user login attempt count equals max-1 ", func() {
withLoginAttempts(maxInvalidLoginAttempts - 1)
err := validateLoginAttempts("user")
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
})
})
Convey("When user login attempt count equals max ", func() {
withLoginAttempts(maxInvalidLoginAttempts)
err := validateLoginAttempts("user")
Convey("it should result in too many login attempts error", func() {
So(err, ShouldEqual, ErrTooManyLoginAttempts)
})
})
Convey("When user login attempt count is greater than max ", func() {
withLoginAttempts(maxInvalidLoginAttempts + 5)
err := validateLoginAttempts("user")
Convey("it should result in too many login attempts error", func() {
So(err, ShouldEqual, ErrTooManyLoginAttempts)
})
})
Convey("When saving invalid login attempt", func() {
defer bus.ClearBusHandlers()
createLoginAttemptCmd := &m.CreateLoginAttemptCommand{}
bus.AddHandler("test", func(cmd *m.CreateLoginAttemptCommand) error {
createLoginAttemptCmd = cmd
return nil
})
saveInvalidLoginAttempt(&LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
})
Convey("it should dispatch command", func() {
So(createLoginAttemptCmd, ShouldNotBeNil)
So(createLoginAttemptCmd.Username, ShouldEqual, "user")
So(createLoginAttemptCmd.IpAddress, ShouldEqual, "192.168.1.1:56433")
})
})
})
Convey("Given brute force login protection disabled", func() {
setting.DisableBruteForceLoginProtection = true
Convey("When user login attempt count equals max-1 ", func() {
withLoginAttempts(maxInvalidLoginAttempts - 1)
err := validateLoginAttempts("user")
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
})
})
Convey("When user login attempt count equals max ", func() {
withLoginAttempts(maxInvalidLoginAttempts)
err := validateLoginAttempts("user")
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
})
})
Convey("When user login attempt count is greater than max ", func() {
withLoginAttempts(maxInvalidLoginAttempts + 5)
err := validateLoginAttempts("user")
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
})
})
Convey("When saving invalid login attempt", func() {
defer bus.ClearBusHandlers()
createLoginAttemptCmd := (*m.CreateLoginAttemptCommand)(nil)
bus.AddHandler("test", func(cmd *m.CreateLoginAttemptCommand) error {
createLoginAttemptCmd = cmd
return nil
})
saveInvalidLoginAttempt(&LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
})
Convey("it should not dispatch command", func() {
So(createLoginAttemptCmd, ShouldBeNil)
})
})
})
})
}
func withLoginAttempts(loginAttempts int64) {
bus.AddHandler("test", func(query *m.GetUserLoginAttemptCountQuery) error {
query.Result = loginAttempts
return nil
})
}

View File

@@ -0,0 +1,35 @@
package login
import (
"crypto/subtle"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
var validatePassword = func(providedPassword string, userPassword string, userSalt string) error {
passwordHashed := util.EncodePassword(providedPassword, userSalt)
if subtle.ConstantTimeCompare([]byte(passwordHashed), []byte(userPassword)) != 1 {
return ErrInvalidCredentials
}
return nil
}
var loginUsingGrafanaDB = func(query *LoginUserQuery) error {
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
if err := bus.Dispatch(&userQuery); err != nil {
return err
}
user := userQuery.Result
if err := validatePassword(query.Password, user.Password, user.Salt); err != nil {
return err
}
query.User = user
return nil
}

View File

@@ -0,0 +1,139 @@
package login
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestGrafanaLogin(t *testing.T) {
Convey("Login using Grafana DB", t, func() {
grafanaLoginScenario("When login with non-existing user", func(sc *grafanaLoginScenarioContext) {
sc.withNonExistingUser()
err := loginUsingGrafanaDB(sc.loginUserQuery)
Convey("it should result in user not found error", func() {
So(err, ShouldEqual, m.ErrUserNotFound)
})
Convey("it should not call password validation", func() {
So(sc.validatePasswordCalled, ShouldBeFalse)
})
Convey("it should not pupulate user object", func() {
So(sc.loginUserQuery.User, ShouldBeNil)
})
})
grafanaLoginScenario("When login with invalid credentials", func(sc *grafanaLoginScenarioContext) {
sc.withInvalidPassword()
err := loginUsingGrafanaDB(sc.loginUserQuery)
Convey("it should result in invalid credentials error", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
})
Convey("it should call password validation", func() {
So(sc.validatePasswordCalled, ShouldBeTrue)
})
Convey("it should not pupulate user object", func() {
So(sc.loginUserQuery.User, ShouldBeNil)
})
})
grafanaLoginScenario("When login with valid credentials", func(sc *grafanaLoginScenarioContext) {
sc.withValidCredentials()
err := loginUsingGrafanaDB(sc.loginUserQuery)
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
})
Convey("it should call password validation", func() {
So(sc.validatePasswordCalled, ShouldBeTrue)
})
Convey("it should pupulate user object", func() {
So(sc.loginUserQuery.User, ShouldNotBeNil)
So(sc.loginUserQuery.User.Login, ShouldEqual, sc.loginUserQuery.Username)
So(sc.loginUserQuery.User.Password, ShouldEqual, sc.loginUserQuery.Password)
})
})
})
}
type grafanaLoginScenarioContext struct {
loginUserQuery *LoginUserQuery
validatePasswordCalled bool
}
type grafanaLoginScenarioFunc func(c *grafanaLoginScenarioContext)
func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) {
Convey(desc, func() {
origValidatePassword := validatePassword
sc := &grafanaLoginScenarioContext{
loginUserQuery: &LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
},
validatePasswordCalled: false,
}
defer func() {
validatePassword = origValidatePassword
}()
fn(sc)
})
}
func mockPasswordValidation(valid bool, sc *grafanaLoginScenarioContext) {
validatePassword = func(providedPassword string, userPassword string, userSalt string) error {
sc.validatePasswordCalled = true
if !valid {
return ErrInvalidCredentials
}
return nil
}
}
func (sc *grafanaLoginScenarioContext) getUserByLoginQueryReturns(user *m.User) {
bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
if user == nil {
return m.ErrUserNotFound
}
query.Result = user
return nil
})
}
func (sc *grafanaLoginScenarioContext) withValidCredentials() {
sc.getUserByLoginQueryReturns(&m.User{
Id: 1,
Login: sc.loginUserQuery.Username,
Password: sc.loginUserQuery.Password,
Salt: "salt",
})
mockPasswordValidation(true, sc)
}
func (sc *grafanaLoginScenarioContext) withNonExistingUser() {
sc.getUserByLoginQueryReturns(nil)
}
func (sc *grafanaLoginScenarioContext) withInvalidPassword() {
sc.getUserByLoginQueryReturns(&m.User{
Password: sc.loginUserQuery.Password,
Salt: "salt",
})
mockPasswordValidation(false, sc)
}

21
pkg/login/ldap_login.go Normal file
View File

@@ -0,0 +1,21 @@
package login
import (
"github.com/grafana/grafana/pkg/setting"
)
var loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
if !setting.LdapEnabled {
return false, nil
}
for _, server := range LdapCfg.Servers {
author := NewLdapAuthenticator(server)
err := author.Login(query)
if err == nil || err != ErrInvalidCredentials {
return true, err
}
}
return true, ErrInvalidCredentials
}

View File

@@ -0,0 +1,172 @@
package login
import (
"testing"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestLdapLogin(t *testing.T) {
Convey("Login using ldap", t, func() {
Convey("Given ldap enabled and a server configured", func() {
setting.LdapEnabled = true
LdapCfg.Servers = append(LdapCfg.Servers,
&LdapServerConf{
Host: "",
})
ldapLoginScenario("When login with invalid credentials", func(sc *ldapLoginScenarioContext) {
sc.withLoginResult(false)
enabled, err := loginUsingLdap(sc.loginUserQuery)
Convey("it should return true", func() {
So(enabled, ShouldBeTrue)
})
Convey("it should return invalid credentials error", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
})
Convey("it should call ldap login", func() {
So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue)
})
})
ldapLoginScenario("When login with valid credentials", func(sc *ldapLoginScenarioContext) {
sc.withLoginResult(true)
enabled, err := loginUsingLdap(sc.loginUserQuery)
Convey("it should return true", func() {
So(enabled, ShouldBeTrue)
})
Convey("it should not return error", func() {
So(err, ShouldBeNil)
})
Convey("it should call ldap login", func() {
So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue)
})
})
})
Convey("Given ldap enabled and no server configured", func() {
setting.LdapEnabled = true
LdapCfg.Servers = make([]*LdapServerConf, 0)
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
sc.withLoginResult(true)
enabled, err := loginUsingLdap(sc.loginUserQuery)
Convey("it should return true", func() {
So(enabled, ShouldBeTrue)
})
Convey("it should return invalid credentials error", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
})
Convey("it should not call ldap login", func() {
So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeFalse)
})
})
})
Convey("Given ldap disabled", func() {
setting.LdapEnabled = false
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
sc.withLoginResult(false)
enabled, err := loginUsingLdap(&LoginUserQuery{
Username: "user",
Password: "pwd",
})
Convey("it should return false", func() {
So(enabled, ShouldBeFalse)
})
Convey("it should not return error", func() {
So(err, ShouldBeNil)
})
Convey("it should not call ldap login", func() {
So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeFalse)
})
})
})
})
}
func mockLdapAuthenticator(valid bool) *mockLdapAuther {
mock := &mockLdapAuther{
validLogin: valid,
}
NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther {
return mock
}
return mock
}
type mockLdapAuther struct {
validLogin bool
loginCalled bool
}
func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
a.loginCalled = true
if !a.validLogin {
return ErrInvalidCredentials
}
return nil
}
func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
return nil
}
func (a *mockLdapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
return nil, nil
}
func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
return nil
}
type ldapLoginScenarioContext struct {
loginUserQuery *LoginUserQuery
ldapAuthenticatorMock *mockLdapAuther
}
type ldapLoginScenarioFunc func(c *ldapLoginScenarioContext)
func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
Convey(desc, func() {
origNewLdapAuthenticator := NewLdapAuthenticator
sc := &ldapLoginScenarioContext{
loginUserQuery: &LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
},
ldapAuthenticatorMock: &mockLdapAuther{},
}
defer func() {
NewLdapAuthenticator = origNewLdapAuthenticator
}()
fn(sc)
})
}
func (sc *ldapLoginScenarioContext) withLoginResult(valid bool) {
sc.ldapAuthenticatorMock = mockLdapAuthenticator(valid)
}

View File

@@ -26,9 +26,10 @@ import (
"strings"
"time"
"context"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/common/model"
"golang.org/x/net/context"
dto "github.com/prometheus/client_model/go"

View File

@@ -379,6 +379,7 @@ func sendUsageStats() {
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
metrics["stats.stars.count"] = statsQuery.Result.Stars
dsStats := models.GetDataSourceStatsQuery{}
if err := bus.Dispatch(&dsStats); err != nil {

View File

@@ -7,6 +7,7 @@ import (
"gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
)
@@ -15,8 +16,8 @@ type AuthOptions struct {
ReqSignedIn bool
}
func getRequestUserId(c *Context) int64 {
userId := c.Session.Get(SESS_KEY_USERID)
func getRequestUserId(c *m.ReqContext) int64 {
userId := c.Session.Get(session.SESS_KEY_USERID)
if userId != nil {
return userId.(int64)
@@ -25,7 +26,7 @@ func getRequestUserId(c *Context) int64 {
return 0
}
func getApiKey(c *Context) string {
func getApiKey(c *m.ReqContext) string {
header := c.Req.Header.Get("Authorization")
parts := strings.SplitN(header, " ", 2)
if len(parts) == 2 && parts[0] == "Bearer" {
@@ -36,28 +37,28 @@ func getApiKey(c *Context) string {
return ""
}
func accessForbidden(c *Context) {
func accessForbidden(c *m.ReqContext) {
if c.IsApiRequest() {
c.JsonApiErr(403, "Permission denied", nil)
return
}
c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/")
c.Redirect(setting.AppSubUrl + "/login")
c.Redirect(setting.AppSubUrl + "/")
}
func notAuthorized(c *Context) {
func notAuthorized(c *m.ReqContext) {
if c.IsApiRequest() {
c.JsonApiErr(401, "Unauthorized", nil)
return
}
c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/")
c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/", nil, false, true)
c.Redirect(setting.AppSubUrl + "/login")
}
func RoleAuth(roles ...m.RoleType) macaron.Handler {
return func(c *Context) {
return func(c *m.ReqContext) {
ok := false
for _, role := range roles {
if role == c.OrgRole {
@@ -72,7 +73,7 @@ func RoleAuth(roles ...m.RoleType) macaron.Handler {
}
func Auth(options *AuthOptions) macaron.Handler {
return func(c *Context) {
return func(c *m.ReqContext) {
if !c.IsSignedIn && options.ReqSignedIn && !c.AllowAnonymous {
notAuthorized(c)
return

View File

@@ -10,10 +10,11 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
)
func initContextWithAuthProxy(ctx *Context, orgId int64) bool {
func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool {
if !setting.AuthProxyEnabled {
return false
}
@@ -58,7 +59,7 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool {
}
// initialize session
if err := ctx.Session.Start(ctx); err != nil {
if err := ctx.Session.Start(ctx.Context); err != nil {
log.Error(3, "Failed to start session", err)
return false
}
@@ -66,12 +67,12 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool {
// Make sure that we cannot share a session between different users!
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
// remove session
if err := ctx.Session.Destory(ctx); err != nil {
if err := ctx.Session.Destory(ctx.Context); err != nil {
log.Error(3, "Failed to destroy session, err")
}
// initialize a new session
if err := ctx.Session.Start(ctx); err != nil {
if err := ctx.Session.Start(ctx.Context); err != nil {
log.Error(3, "Failed to start session", err)
}
}
@@ -89,17 +90,17 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool {
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
ctx.Session.Set(SESS_KEY_USERID, ctx.UserId)
ctx.Session.Set(session.SESS_KEY_USERID, ctx.UserId)
return true
}
var syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQuery) error {
var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
if setting.LdapEnabled {
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
var lastLdapSync int64
if lastLdapSyncInSession := ctx.Session.Get(SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
lastLdapSync = lastLdapSyncInSession.(int64)
}
@@ -113,14 +114,14 @@ var syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQue
}
}
ctx.Session.Set(SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
}
}
return nil
}
func checkAuthenticationProxy(ctx *Context, proxyHeaderValue string) error {
func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error {
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) > 0 {
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")

View File

@@ -6,8 +6,10 @@ import (
"github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
)
func TestAuthProxyWithLdapEnabled(t *testing.T) {
@@ -29,45 +31,45 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) {
Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() {
// arrange
session := mockSession{}
ctx := Context{Session: &session}
So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
sess := mockSession{}
ctx := m.ReqContext{Session: &sess}
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
// act
syncGrafanaUserWithLdapUser(&ctx, &query)
// assert
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
So(session.Get(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() {
// arrange
session := mockSession{}
ctx := Context{Session: &session}
sess := mockSession{}
ctx := m.ReqContext{Session: &sess}
now := time.Now().Unix()
session.Set(SESS_KEY_LASTLDAPSYNC, now)
sess.Set(session.SESS_KEY_LASTLDAPSYNC, now)
// act
syncGrafanaUserWithLdapUser(&ctx, &query)
// assert
So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse)
})
Convey("When lastldapsync is expired, session variable should be updated", func() {
// arrange
session := mockSession{}
ctx := Context{Session: &session}
sess := mockSession{}
ctx := m.ReqContext{Session: &sess}
expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
session.Set(SESS_KEY_LASTLDAPSYNC, expiredTime)
sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime)
// act
syncGrafanaUserWithLdapUser(&ctx, &query)
// assert
So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
})
})
@@ -77,7 +79,7 @@ type mockSession struct {
value interface{}
}
func (s *mockSession) Start(c *Context) error {
func (s *mockSession) Start(c *macaron.Context) error {
return nil
}
@@ -102,11 +104,11 @@ func (s *mockSession) Release() error {
return nil
}
func (s *mockSession) Destory(c *Context) error {
func (s *mockSession) Destory(c *macaron.Context) error {
return nil
}
func (s *mockSession) RegenerateId(c *Context) error {
func (s *mockSession) RegenerateId(c *macaron.Context) error {
return nil
}

View File

@@ -0,0 +1,49 @@
package middleware
import (
"fmt"
"strings"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"gopkg.in/macaron.v1"
)
func getDashboardUrlBySlug(orgId int64, slug string) (string, error) {
query := m.GetDashboardQuery{Slug: slug, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
return "", m.ErrDashboardNotFound
}
return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil
}
func RedirectFromLegacyDashboardUrl() macaron.Handler {
return func(c *m.ReqContext) {
slug := c.Params("slug")
if slug != "" {
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
c.Redirect(url, 301)
return
}
}
}
}
func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
return func(c *m.ReqContext) {
slug := c.Params("slug")
if slug != "" {
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
url = strings.Replace(url, "/d/", "/d-solo/", 1)
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
c.Redirect(url, 301)
return
}
}
}
}

View File

@@ -0,0 +1,58 @@
package middleware
import (
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
. "github.com/smartystreets/goconvey/convey"
)
func TestMiddlewareDashboardRedirect(t *testing.T) {
Convey("Given the dashboard redirect middleware", t, func() {
bus.ClearBusHandlers()
redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl()
redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl()
fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 1
fakeDash.FolderId = 1
fakeDash.HasAcl = false
fakeDash.Uid = util.GenerateShortUid()
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
middlewareScenario("GET dashboard by legacy url", func(sc *scenarioContext) {
sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler)
sc.fakeReqWithParams("GET", "/dashboard/db/dash?orgId=1&panelId=2", map[string]string{}).exec()
Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
So(sc.resp.Code, ShouldEqual, 301)
redirectUrl, _ := sc.resp.Result().Location()
So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug))
So(len(redirectUrl.Query()), ShouldEqual, 2)
})
})
middlewareScenario("GET dashboard solo by legacy url", func(sc *scenarioContext) {
sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler)
sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash?orgId=1&panelId=2", map[string]string{}).exec()
Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
So(sc.resp.Code, ShouldEqual, 301)
redirectUrl, _ := sc.resp.Result().Location()
expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1)
So(redirectUrl.Path, ShouldEqual, expectedUrl)
So(len(redirectUrl.Query()), ShouldEqual, 2)
})
})
})
}

View File

@@ -19,6 +19,7 @@ import (
"net/http"
"time"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/macaron.v1"
@@ -47,7 +48,7 @@ func Logger() macaron.Handler {
}
if ctx, ok := c.Data["ctx"]; ok {
ctxTyped := ctx.(*Context)
ctxTyped := ctx.(*m.ReqContext)
if status == 500 {
ctxTyped.Logger.Error("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ms", int64(timeTakenMs), "size", rw.Size(), "referer", req.Referer())
} else {

View File

@@ -2,7 +2,6 @@ package middleware
import (
"strconv"
"strings"
"gopkg.in/macaron.v1"
@@ -11,29 +10,17 @@ import (
"github.com/grafana/grafana/pkg/log"
l "github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/prometheus/client_golang/prometheus"
)
type Context struct {
*macaron.Context
*m.SignedInUser
Session SessionStore
IsSignedIn bool
IsRenderCall bool
AllowAnonymous bool
Logger log.Logger
}
func GetContextHandler() macaron.Handler {
return func(c *macaron.Context) {
ctx := &Context{
ctx := &m.ReqContext{
Context: c,
SignedInUser: &m.SignedInUser{},
Session: GetSession(),
Session: session.GetSession(),
IsSignedIn: false,
AllowAnonymous: false,
Logger: log.New("context"),
@@ -74,7 +61,7 @@ func GetContextHandler() macaron.Handler {
}
}
func initContextWithAnonymousUser(ctx *Context) bool {
func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
if !setting.AnonymousEnabled {
return false
}
@@ -94,9 +81,9 @@ func initContextWithAnonymousUser(ctx *Context) bool {
return true
}
func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool {
func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool {
// initialize session
if err := ctx.Session.Start(ctx); err != nil {
if err := ctx.Session.Start(ctx.Context); err != nil {
ctx.Logger.Error("Failed to start session", "error", err)
return false
}
@@ -117,7 +104,7 @@ func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool {
return true
}
func initContextWithApiKey(ctx *Context) bool {
func initContextWithApiKey(ctx *m.ReqContext) bool {
var keyString string
if keyString = getApiKey(ctx); keyString == "" {
return false
@@ -153,7 +140,7 @@ func initContextWithApiKey(ctx *Context) bool {
return true
}
func initContextWithBasicAuth(ctx *Context, orgId int64) bool {
func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
if !setting.BasicAuthEnabled {
return false
@@ -195,68 +182,8 @@ func initContextWithBasicAuth(ctx *Context, orgId int64) bool {
return true
}
// Handle handles and logs error by given status.
func (ctx *Context) Handle(status int, title string, err error) {
if err != nil {
ctx.Logger.Error(title, "error", err)
if setting.Env != setting.PROD {
ctx.Data["ErrorMsg"] = err
}
}
ctx.Data["Title"] = title
ctx.Data["AppSubUrl"] = setting.AppSubUrl
ctx.HTML(status, strconv.Itoa(status))
}
func (ctx *Context) JsonOK(message string) {
resp := make(map[string]interface{})
resp["message"] = message
ctx.JSON(200, resp)
}
func (ctx *Context) IsApiRequest() bool {
return strings.HasPrefix(ctx.Req.URL.Path, "/api")
}
func (ctx *Context) JsonApiErr(status int, message string, err error) {
resp := make(map[string]interface{})
if err != nil {
ctx.Logger.Error(message, "error", err)
if setting.Env != setting.PROD {
resp["error"] = err.Error()
}
}
switch status {
case 404:
resp["message"] = "Not Found"
case 500:
resp["message"] = "Internal Server Error"
}
if message != "" {
resp["message"] = message
}
ctx.JSON(status, resp)
}
func (ctx *Context) HasUserRole(role m.RoleType) bool {
return ctx.OrgRole.Includes(role)
}
func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool {
return ctx.HelpFlags1.HasFlag(flag)
}
func (ctx *Context) TimeRequest(timer prometheus.Summary) {
ctx.Data["perfmon.timer"] = timer
}
func AddDefaultResponseHeaders() macaron.Handler {
return func(ctx *Context) {
return func(ctx *m.ReqContext) {
if ctx.IsApiRequest() && ctx.Req.Method == "GET" {
ctx.Resp.Header().Add("Cache-Control", "no-cache")
ctx.Resp.Header().Add("Pragma", "no-cache")

View File

@@ -7,10 +7,11 @@ import (
"path/filepath"
"testing"
"github.com/go-macaron/session"
ms "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
l "github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
. "github.com/smartystreets/goconvey/convey"
@@ -130,8 +131,8 @@ func TestMiddlewareContext(t *testing.T) {
middlewareScenario("UserId in session", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *Context) {
c.Session.Set(SESS_KEY_USERID, int64(12))
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
@@ -276,8 +277,8 @@ func TestMiddlewareContext(t *testing.T) {
})
// create session
sc.fakeReq("GET", "/").handler(func(c *Context) {
c.Session.Set(SESS_KEY_USERID, int64(33))
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(33))
}).exec()
oldSessionID := sc.context.Session.ID()
@@ -300,7 +301,7 @@ func TestMiddlewareContext(t *testing.T) {
setting.LdapEnabled = true
called := false
syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQuery) error {
syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
called = true
return nil
}
@@ -336,12 +337,12 @@ func middlewareScenario(desc string, fn scenarioFunc) {
sc.m.Use(GetContextHandler())
// mock out gc goroutine
startSessionGC = func() {}
sc.m.Use(Sessioner(&session.Options{}))
session.StartSessionGC = func() {}
sc.m.Use(Sessioner(&ms.Options{}))
sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders())
sc.defaultHandler = func(c *Context) {
sc.defaultHandler = func(c *m.ReqContext) {
sc.context = c
if sc.handlerFunc != nil {
sc.handlerFunc(sc.context)
@@ -356,7 +357,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
type scenarioContext struct {
m *macaron.Macaron
context *Context
context *m.ReqContext
resp *httptest.ResponseRecorder
apiKey string
authHeader string
@@ -399,6 +400,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
So(err, ShouldBeNil)
sc.req = req
return sc
}
func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext {
sc.handlerFunc = fn
return sc
@@ -422,4 +437,4 @@ func (sc *scenarioContext) exec() {
}
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *Context)
type handlerFunc func(c *m.ReqContext)

View File

@@ -7,7 +7,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1"
@@ -22,7 +22,7 @@ func OrgRedirect() macaron.Handler {
return
}
ctx, ok := c.Data["ctx"].(*Context)
ctx, ok := c.Data["ctx"].(*m.ReqContext)
if !ok || !ctx.IsSignedIn {
return
}
@@ -31,7 +31,7 @@ func OrgRedirect() macaron.Handler {
return
}
cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId}
cmd := m.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil {
if ctx.IsApiRequest() {
ctx.JsonApiErr(404, "Not found", nil)

View File

@@ -6,7 +6,8 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
. "github.com/smartystreets/goconvey/convey"
)
@@ -14,16 +15,16 @@ func TestOrgRedirectMiddleware(t *testing.T) {
Convey("Can redirect to correct org", t, func() {
middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *Context) {
c.Session.Set(SESS_KEY_USERID, int64(12))
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return nil
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
@@ -36,16 +37,16 @@ func TestOrgRedirectMiddleware(t *testing.T) {
})
middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *Context) {
c.Session.Set(SESS_KEY_USERID, int64(12))
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
return nil
})

View File

@@ -4,9 +4,11 @@ import (
"net/http"
"gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
)
func MeasureRequestTime() macaron.Handler {
return func(res http.ResponseWriter, req *http.Request, c *Context) {
return func(res http.ResponseWriter, req *http.Request, c *m.ReqContext) {
}
}

View File

@@ -3,15 +3,15 @@ package middleware
import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
)
func Quota(target string) macaron.Handler {
return func(c *Context) {
limitReached, err := QuotaReached(c, target)
return func(c *m.ReqContext) {
limitReached, err := quota.QuotaReached(c, target)
if err != nil {
c.JsonApiErr(500, "failed to get quota", err)
return
@@ -22,82 +22,3 @@ func Quota(target string) macaron.Handler {
}
}
}
func QuotaReached(c *Context, target string) (bool, error) {
if !setting.Quota.Enabled {
return false, nil
}
// get the list of scopes that this target is valid for. Org, User, Global
scopes, err := m.GetQuotaScopes(target)
if err != nil {
return false, err
}
for _, scope := range scopes {
c.Logger.Debug("Checking quota", "target", target, "scope", scope)
switch scope.Name {
case "global":
if scope.DefaultLimit < 0 {
continue
}
if scope.DefaultLimit == 0 {
return true, nil
}
if target == "session" {
usedSessions := getSessionCount()
if int64(usedSessions) > scope.DefaultLimit {
c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
return true, nil
}
continue
}
query := m.GetGlobalQuotaByTargetQuery{Target: scope.Target}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
if query.Result.Used >= scope.DefaultLimit {
return true, nil
}
case "org":
if !c.IsSignedIn {
continue
}
query := m.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
case "user":
if !c.IsSignedIn || c.UserId == 0 {
continue
}
query := m.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
}
}
return false, nil
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -12,7 +13,7 @@ import (
func TestMiddlewareQuota(t *testing.T) {
Convey("Given the grafana quota middleware", t, func() {
getSessionCount = func() int {
session.GetSessionCount = func() int {
return 4
}
@@ -74,8 +75,8 @@ func TestMiddlewareQuota(t *testing.T) {
middlewareScenario("with user logged in", func(sc *scenarioContext) {
// log us in, so we have a user_id and org_id in the context
sc.fakeReq("GET", "/").handler(func(c *Context) {
c.Session.Set(SESS_KEY_USERID, int64(12))
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {

View File

@@ -24,6 +24,7 @@ import (
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
@@ -106,7 +107,7 @@ func Recovery() macaron.Handler {
panicLogger := log.Root
// try to get request logger
if ctx, ok := c.Data["ctx"]; ok {
ctxTyped := ctx.(*Context)
ctxTyped := ctx.(*m.ReqContext)
panicLogger = ctxTyped.Logger
}
@@ -115,15 +116,15 @@ func Recovery() macaron.Handler {
c.Data["Title"] = "Server Error"
c.Data["AppSubUrl"] = setting.AppSubUrl
if theErr, ok := err.(error); ok {
c.Data["Title"] = theErr.Error()
}
if setting.Env == setting.DEV {
if theErr, ok := err.(error); ok {
c.Data["Title"] = theErr.Error()
}
c.Data["ErrorMsg"] = string(stack)
}
ctx, ok := c.Data["ctx"].(*Context)
ctx, ok := c.Data["ctx"].(*m.ReqContext)
if ok && ctx.IsApiRequest() {
resp := make(map[string]interface{})
@@ -137,7 +138,7 @@ func Recovery() macaron.Handler {
c.JSON(500, resp)
} else {
c.HTML(500, "500")
c.HTML(500, "error")
}
}
}()

View File

@@ -4,8 +4,10 @@ import (
"path/filepath"
"testing"
"github.com/go-macaron/session"
ms "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
)
@@ -37,7 +39,7 @@ func TestRecoveryMiddleware(t *testing.T) {
})
}
func PanicHandler(c *Context) {
func PanicHandler(c *m.ReqContext) {
panic("Handler has panicked")
}
@@ -60,12 +62,12 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
sc.m.Use(GetContextHandler())
// mock out gc goroutine
startSessionGC = func() {}
sc.m.Use(Sessioner(&session.Options{}))
session.StartSessionGC = func() {}
sc.m.Use(Sessioner(&ms.Options{}))
sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders())
sc.defaultHandler = func(c *Context) {
sc.defaultHandler = func(c *m.ReqContext) {
sc.context = c
if sc.handlerFunc != nil {
sc.handlerFunc(sc.context)

View File

@@ -10,7 +10,7 @@ import (
var renderKeysLock sync.Mutex
var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser)
func initContextWithRenderAuth(ctx *Context) bool {
func initContextWithRenderAuth(ctx *m.ReqContext) bool {
key := ctx.GetCookie("renderKey")
if key == "" {
return false

View File

@@ -1,170 +1,21 @@
package middleware
import (
"math/rand"
"time"
"github.com/go-macaron/session"
_ "github.com/go-macaron/session/memcache"
_ "github.com/go-macaron/session/mysql"
_ "github.com/go-macaron/session/postgres"
_ "github.com/go-macaron/session/redis"
"github.com/grafana/grafana/pkg/log"
ms "github.com/go-macaron/session"
"gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
)
const (
SESS_KEY_USERID = "uid"
SESS_KEY_OAUTH_STATE = "state"
SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys
SESS_KEY_LASTLDAPSYNC = "last_ldap_sync"
)
func Sessioner(options *ms.Options) macaron.Handler {
session.Init(options)
var sessionManager *session.Manager
var sessionOptions *session.Options
var startSessionGC func()
var getSessionCount func() int
var sessionLogger = log.New("session")
func init() {
startSessionGC = func() {
sessionManager.GC()
sessionLogger.Debug("Session GC")
time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC)
}
getSessionCount = func() int {
return sessionManager.Count()
}
}
func prepareOptions(opt *session.Options) *session.Options {
if len(opt.Provider) == 0 {
opt.Provider = "memory"
}
if len(opt.ProviderConfig) == 0 {
opt.ProviderConfig = "data/sessions"
}
if len(opt.CookieName) == 0 {
opt.CookieName = "grafana_sess"
}
if len(opt.CookiePath) == 0 {
opt.CookiePath = "/"
}
if opt.Gclifetime == 0 {
opt.Gclifetime = 3600
}
if opt.Maxlifetime == 0 {
opt.Maxlifetime = opt.Gclifetime
}
if opt.IDLength == 0 {
opt.IDLength = 16
}
return opt
}
func Sessioner(options *session.Options) macaron.Handler {
var err error
sessionOptions = prepareOptions(options)
sessionManager, err = session.NewManager(options.Provider, *options)
if err != nil {
panic(err)
}
// start GC threads after some random seconds
rndSeconds := 10 + rand.Int63n(180)
time.AfterFunc(time.Duration(rndSeconds)*time.Second, startSessionGC)
return func(ctx *Context) {
return func(ctx *m.ReqContext) {
ctx.Next()
if err = ctx.Session.Release(); err != nil {
if err := ctx.Session.Release(); err != nil {
panic("session(release): " + err.Error())
}
}
}
func GetSession() SessionStore {
return &SessionWrapper{manager: sessionManager}
}
type SessionStore interface {
// Set sets value to given key in session.
Set(interface{}, interface{}) error
// Get gets value by given key in session.
Get(interface{}) interface{}
// Delete deletes a key from session.
Delete(interface{}) interface{}
// ID returns current session ID.
ID() string
// Release releases session resource and save data to provider.
Release() error
// Destory deletes a session.
Destory(*Context) error
// init
Start(*Context) error
// RegenerateId regenerates the session id
RegenerateId(*Context) error
}
type SessionWrapper struct {
session session.RawStore
manager *session.Manager
}
func (s *SessionWrapper) Start(c *Context) error {
var err error
s.session, err = s.manager.Start(c.Context)
return err
}
func (s *SessionWrapper) RegenerateId(c *Context) error {
var err error
s.session, err = s.manager.RegenerateId(c.Context)
return err
}
func (s *SessionWrapper) Set(k interface{}, v interface{}) error {
if s.session != nil {
return s.session.Set(k, v)
}
return nil
}
func (s *SessionWrapper) Get(k interface{}) interface{} {
if s.session != nil {
return s.session.Get(k)
}
return nil
}
func (s *SessionWrapper) Delete(k interface{}) interface{} {
if s.session != nil {
return s.session.Delete(k)
}
return nil
}
func (s *SessionWrapper) ID() string {
if s.session != nil {
return s.session.ID()
}
return ""
}
func (s *SessionWrapper) Release() error {
if s.session != nil {
return s.session.Release()
}
return nil
}
func (s *SessionWrapper) Destory(c *Context) error {
if s.session != nil {
if err := s.manager.Destory(c.Context); err != nil {
return err
}
s.session = nil
}
return nil
}

View File

@@ -3,12 +3,13 @@ package middleware
import (
"strings"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1"
)
func ValidateHostHeader(domain string) macaron.Handler {
return func(c *Context) {
return func(c *m.ReqContext) {
// ignore local render calls
if c.IsRenderCall {
return

View File

@@ -159,10 +159,6 @@ type SetAlertStateCommand struct {
Timestamp time.Time
}
type DeleteAlertCommand struct {
AlertId int64
}
//Queries
type GetAlertsQuery struct {
OrgId int64
@@ -170,8 +166,9 @@ type GetAlertsQuery struct {
DashboardId int64
PanelId int64
Limit int64
User *SignedInUser
Result []*Alert
Result []*AlertListItemDTO
}
type GetAllAlertsQuery struct {
@@ -191,6 +188,21 @@ type GetAlertStatesForDashboardQuery struct {
Result []*AlertStateInfoDTO
}
type AlertListItemDTO struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
DashboardUid string `json:"dashboardUid"`
DashboardSlug string `json:"dashboardSlug"`
PanelId int64 `json:"panelId"`
Name string `json:"name"`
State AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
EvalDate time.Time `json:"evalDate"`
EvalData *simplejson.Json `json:"evalData"`
ExecutionError string `json:"executionError"`
Url string `json:"url"`
}
type AlertStateInfoDTO struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
@@ -198,3 +210,17 @@ type AlertStateInfoDTO struct {
State AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
}
// "Internal" commands
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *Dashboard
}
type ValidateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *Dashboard
}

86
pkg/models/context.go Normal file
View File

@@ -0,0 +1,86 @@
package models
import (
"strings"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
)
type ReqContext struct {
*macaron.Context
*SignedInUser
Session session.SessionStore
IsSignedIn bool
IsRenderCall bool
AllowAnonymous bool
Logger log.Logger
}
// Handle handles and logs error by given status.
func (ctx *ReqContext) Handle(status int, title string, err error) {
if err != nil {
ctx.Logger.Error(title, "error", err)
if setting.Env != setting.PROD {
ctx.Data["ErrorMsg"] = err
}
}
ctx.Data["Title"] = title
ctx.Data["AppSubUrl"] = setting.AppSubUrl
ctx.Data["Theme"] = "dark"
ctx.HTML(status, "error")
}
func (ctx *ReqContext) JsonOK(message string) {
resp := make(map[string]interface{})
resp["message"] = message
ctx.JSON(200, resp)
}
func (ctx *ReqContext) IsApiRequest() bool {
return strings.HasPrefix(ctx.Req.URL.Path, "/api")
}
func (ctx *ReqContext) JsonApiErr(status int, message string, err error) {
resp := make(map[string]interface{})
if err != nil {
ctx.Logger.Error(message, "error", err)
if setting.Env != setting.PROD {
resp["error"] = err.Error()
}
}
switch status {
case 404:
resp["message"] = "Not Found"
case 500:
resp["message"] = "Internal Server Error"
}
if message != "" {
resp["message"] = message
}
ctx.JSON(status, resp)
}
func (ctx *ReqContext) HasUserRole(role RoleType) bool {
return ctx.OrgRole.Includes(role)
}
func (ctx *ReqContext) HasHelpFlag(flag HelpFlags1) bool {
return ctx.HelpFlags1.HasFlag(flag)
}
func (ctx *ReqContext) TimeRequest(timer prometheus.Summary) {
ctx.Data["perfmon.timer"] = timer
}

View File

@@ -26,6 +26,8 @@ func (p PermissionType) String() string {
var (
ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.")
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
ErrFolderAclInfoMissing = errors.New("User id and team id cannot both be empty for a folder permission.")
ErrFolderPermissionFolderEmpty = errors.New("Folder Id must be greater than zero for a folder permission.")
)
// Dashboard ACL model
@@ -44,9 +46,9 @@ type DashboardAcl struct {
}
type DashboardAclInfoDTO struct {
Id int64 `json:"id"`
OrgId int64 `json:"-"`
DashboardId int64 `json:"dashboardId"`
DashboardId int64 `json:"dashboardId,omitempty"`
FolderId int64 `json:"folderId,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
@@ -59,6 +61,32 @@ type DashboardAclInfoDTO struct {
Role *RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"`
Uid string `json:"uid"`
Title string `json:"title"`
Slug string `json:"slug"`
IsFolder bool `json:"isFolder"`
Url string `json:"url"`
}
func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool {
if dto.Role == nil || other.Role == nil {
return false
}
return dto.UserId <= 0 && dto.TeamId <= 0 && dto.UserId == other.UserId && dto.TeamId == other.TeamId && *dto.Role == *other.Role
}
func (dto *DashboardAclInfoDTO) hasSameUserAs(other *DashboardAclInfoDTO) bool {
return dto.UserId > 0 && dto.UserId == other.UserId
}
func (dto *DashboardAclInfoDTO) hasSameTeamAs(other *DashboardAclInfoDTO) bool {
return dto.TeamId > 0 && dto.TeamId == other.TeamId
}
// IsDuplicateOf returns true if other item has same role, same user or same team
func (dto *DashboardAclInfoDTO) IsDuplicateOf(other *DashboardAclInfoDTO) bool {
return dto.hasSameRoleAs(other) || dto.hasSameUserAs(other) || dto.hasSameTeamAs(other)
}
//
@@ -70,21 +98,6 @@ type UpdateDashboardAclCommand struct {
Items []*DashboardAcl
}
type SetDashboardAclCommand struct {
DashboardId int64
OrgId int64
UserId int64
TeamId int64
Permission PermissionType
Result DashboardAcl
}
type RemoveDashboardAclCommand struct {
AclId int64
OrgId int64
}
//
// QUERIES
//

View File

@@ -64,10 +64,12 @@ type DeleteDashboardSnapshotCommand struct {
}
type DeleteExpiredSnapshotsCommand struct {
DeletedRows int64
}
type GetDashboardSnapshotQuery struct {
Key string
Key string
DeleteKey string
Result *DashboardSnapshot
}
@@ -76,9 +78,10 @@ type DashboardSnapshots []*DashboardSnapshot
type DashboardSnapshotsList []*DashboardSnapshotDTO
type GetDashboardSnapshotsQuery struct {
Name string
Limit int
OrgId int64
Name string
Limit int
OrgId int64
SignedInUser *SignedInUser
Result DashboardSnapshotsList
}

View File

@@ -75,4 +75,5 @@ type GetDashboardVersionsQuery struct {
//
type DeleteExpiredVersionsCommand struct {
DeletedRows int64
}

View File

@@ -2,23 +2,37 @@ package models
import (
"errors"
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/setting"
)
// Typed errors
var (
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardFolderNotFound = errors.New("Folder not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
RootFolderName = "General"
)
type UpdatePluginDashboardError struct {
@@ -39,6 +53,7 @@ var (
// Dashboard model
type Dashboard struct {
Id int64
Uid string
Slug string
OrgId int64
GnetId int64
@@ -58,6 +73,30 @@ type Dashboard struct {
Data *simplejson.Json
}
func (d *Dashboard) SetId(id int64) {
d.Id = id
d.Data.Set("id", id)
}
func (d *Dashboard) SetUid(uid string) {
d.Uid = uid
d.Data.Set("uid", uid)
}
func (d *Dashboard) SetVersion(version int) {
d.Version = version
d.Data.Set("version", version)
}
// GetDashboardIdForSavePermissionCheck return the dashboard id to be used for checking permission of dashboard
func (d *Dashboard) GetDashboardIdForSavePermissionCheck() int64 {
if d.Id == 0 {
return d.FolderId
}
return d.Id
}
// NewDashboard creates a new dashboard
func NewDashboard(title string) *Dashboard {
dash := &Dashboard{}
@@ -73,9 +112,10 @@ func NewDashboard(title string) *Dashboard {
// NewDashboardFolder creates a new dashboard folder
func NewDashboardFolder(title string) *Dashboard {
folder := NewDashboard(title)
folder.IsFolder = true
folder.Data.Set("schemaVersion", 16)
folder.Data.Set("editable", true)
folder.Data.Set("hideControls", true)
folder.Data.Set("version", 0)
folder.IsFolder = true
return folder
}
@@ -89,14 +129,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.Data = data
dash.Title = dash.Data.Get("title").MustString()
dash.UpdateSlug()
update := false
if id, err := dash.Data.Get("id").Float64(); err == nil {
dash.Id = int64(id)
update = true
}
if version, err := dash.Data.Get("version").Float64(); err == nil {
dash.Version = int(version)
dash.Updated = time.Now()
}
if uid, err := dash.Data.Get("uid").String(); err == nil {
dash.Uid = uid
update = true
}
if version, err := dash.Data.Get("version").Float64(); err == nil && update {
dash.Version = int(version)
dash.Updated = time.Now()
} else {
dash.Data.Set("version", 0)
dash.Created = time.Now()
@@ -119,10 +166,6 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
userId = -1
}
if dash.Data.Get("version").MustInt(0) == 0 {
dash.CreatedBy = userId
}
dash.UpdatedBy = userId
dash.OrgId = cmd.OrgId
dash.PluginId = cmd.PluginId
@@ -147,6 +190,40 @@ func SlugifyTitle(title string) string {
return slug.Make(strings.ToLower(title))
}
// GetUrl return the html url for a folder if it's folder, otherwise for a dashboard
func (dash *Dashboard) GetUrl() string {
return GetDashboardFolderUrl(dash.IsFolder, dash.Uid, dash.Slug)
}
// Return the html url for a dashboard
func (dash *Dashboard) GenerateUrl() string {
return GetDashboardUrl(dash.Uid, dash.Slug)
}
// GetDashboardFolderUrl return the html url for a folder if it's folder, otherwise for a dashboard
func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string {
if isFolder {
return GetFolderUrl(uid, slug)
}
return GetDashboardUrl(uid, slug)
}
// Return the html url for a dashboard
func GetDashboardUrl(uid string, slug string) string {
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
}
// Return the full url for a dashboard
func GetFullDashboardUrl(uid string, slug string) string {
return fmt.Sprintf("%s%s", setting.AppUrl, GetDashboardUrl(uid, slug))
}
// GetFolderUrl return the html url for a folder
func GetFolderUrl(folderUid string, slug string) string {
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
}
//
// COMMANDS
//
@@ -167,18 +244,40 @@ type SaveDashboardCommand struct {
Result *Dashboard
}
type DashboardProvisioning struct {
Id int64
DashboardId int64
Name string
ExternalId string
Updated int64
}
type SaveProvisionedDashboardCommand struct {
DashboardCmd *SaveDashboardCommand
DashboardProvisioning *DashboardProvisioning
Result *Dashboard
}
type DeleteDashboardCommand struct {
Id int64
OrgId int64
}
type ValidateDashboardBeforeSaveCommand struct {
OrgId int64
Dashboard *Dashboard
Overwrite bool
}
//
// QUERIES
//
type GetDashboardQuery struct {
Slug string // required if no Id is specified
Slug string // required if no Id or Uid is specified
Id int64 // optional if slug is set
Uid string // optional if slug is set
OrgId int64
Result *Dashboard
@@ -199,6 +298,14 @@ type GetDashboardsQuery struct {
Result []*Dashboard
}
type GetDashboardPermissionsForUserQuery struct {
DashboardIds []int64
OrgId int64
UserId int64
OrgRole RoleType
Result []*DashboardPermissionForUser
}
type GetDashboardsByPluginIdQuery struct {
OrgId int64
PluginId string
@@ -209,3 +316,32 @@ type GetDashboardSlugByIdQuery struct {
Id int64
Result string
}
type GetProvisionedDashboardDataQuery struct {
Name string
Result []*DashboardProvisioning
}
type GetDashboardsBySlugQuery struct {
OrgId int64
Slug string
Result []*Dashboard
}
type DashboardPermissionForUser struct {
DashboardId int64 `json:"dashboardId"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"`
}
type DashboardRef struct {
Uid string
Slug string
}
type GetDashboardRefByIdQuery struct {
Id int64
Result *DashboardRef
}

Some files were not shown because too many files have changed in this diff Show More