mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
merge with master
This commit is contained in:
@@ -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{}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
96
pkg/api/alerting_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -6,11 +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,
|
||||
@@ -21,6 +23,7 @@ func GetAnnotations(c *middleware.Context) Response {
|
||||
PanelId: c.QueryInt64("panelId"),
|
||||
Limit: c.QueryInt64("limit"),
|
||||
Tags: c.QueryStrings("tags"),
|
||||
Type: c.Query("type"),
|
||||
}
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
@@ -48,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 == "" {
|
||||
@@ -75,9 +82,11 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
||||
return ApiError(500, "Failed to save annotation", err)
|
||||
}
|
||||
|
||||
startID := item.Id
|
||||
|
||||
// handle regions
|
||||
if cmd.IsRegion {
|
||||
item.RegionId = item.Id
|
||||
item.RegionId = startID
|
||||
|
||||
if item.Data == nil {
|
||||
item.Data = simplejson.New()
|
||||
@@ -93,9 +102,18 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
||||
if err := repo.Save(&item); err != nil {
|
||||
return ApiError(500, "Failed save annotation for region end time", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{
|
||||
"message": "Annotation added",
|
||||
"id": startID,
|
||||
"endId": item.Id,
|
||||
})
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotation added")
|
||||
return Json(200, util.DynMap{
|
||||
"message": "Annotation added",
|
||||
"id": startID,
|
||||
})
|
||||
}
|
||||
|
||||
func formatGraphiteAnnotation(what string, data string) string {
|
||||
@@ -106,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 == "" {
|
||||
@@ -154,14 +172,21 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati
|
||||
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Graphite annotation added")
|
||||
return Json(200, util.DynMap{
|
||||
"message": "Graphite annotation added",
|
||||
"id": item.Id,
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -192,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{
|
||||
@@ -208,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,
|
||||
})
|
||||
@@ -223,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,
|
||||
})
|
||||
@@ -237,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
241
pkg/api/annotations_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -40,9 +42,14 @@ func (hs *HttpServer) registerRoutes() {
|
||||
r.Get("/datasources/", reqSignedIn, Index)
|
||||
r.Get("/datasources/new", reqSignedIn, Index)
|
||||
r.Get("/datasources/edit/*", reqSignedIn, Index)
|
||||
r.Get("/org/users/", reqSignedIn, Index)
|
||||
r.Get("/org/users", reqSignedIn, Index)
|
||||
r.Get("/org/users/new", reqSignedIn, Index)
|
||||
r.Get("/org/users/invite", reqSignedIn, Index)
|
||||
r.Get("/org/teams", reqSignedIn, Index)
|
||||
r.Get("/org/teams/*", reqSignedIn, Index)
|
||||
r.Get("/org/apikeys/", reqSignedIn, Index)
|
||||
r.Get("/dashboard/import/", reqSignedIn, Index)
|
||||
r.Get("/configuration", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/settings", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/users", reqGrafanaAdmin, Index)
|
||||
@@ -58,10 +65,16 @@ 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("/d/:uid", 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)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, Index)
|
||||
@@ -94,7 +107,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)
|
||||
@@ -134,6 +147,18 @@ func (hs *HttpServer) registerRoutes() {
|
||||
usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
|
||||
}, reqGrafanaAdmin)
|
||||
|
||||
// team (admin permission required)
|
||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
||||
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||
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", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// org information available to all users.
|
||||
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
||||
orgRoute.Get("/", wrap(GetOrgCurrent))
|
||||
@@ -199,10 +224,10 @@ func (hs *HttpServer) registerRoutes() {
|
||||
// Data sources
|
||||
apiRoute.Group("/datasources", func(datasourceRoute RouteRegister) {
|
||||
datasourceRoute.Get("/", wrap(GetDataSources))
|
||||
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
|
||||
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource))
|
||||
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
|
||||
datasourceRoute.Delete("/:id", DeleteDataSourceById)
|
||||
datasourceRoute.Delete("/name/:name", DeleteDataSourceByName)
|
||||
datasourceRoute.Delete("/:id", wrap(DeleteDataSourceById))
|
||||
datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName))
|
||||
datasourceRoute.Get("/:id", wrap(GetDataSourceById))
|
||||
datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName))
|
||||
}, reqOrgAdmin)
|
||||
@@ -222,22 +247,49 @@ 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("/db/:slug", GetDashboard)
|
||||
dashboardRoute.Delete("/db/:slug", reqEditorRole, DeleteDashboard)
|
||||
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
|
||||
dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid))
|
||||
|
||||
dashboardRoute.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
|
||||
dashboardRoute.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
|
||||
dashboardRoute.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
|
||||
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.Get("/file/:file", GetDashboardFromJsonFile)
|
||||
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))
|
||||
|
||||
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
|
||||
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
|
||||
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
|
||||
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
|
||||
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute RouteRegister) {
|
||||
dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
|
||||
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Dashboard snapshots
|
||||
@@ -267,7 +319,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
apiRoute.Group("/alerts", func(alertsRoute RouteRegister) {
|
||||
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
|
||||
alertsRoute.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole)
|
||||
alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
|
||||
alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
|
||||
alertsRoute.Get("/", wrap(GetAlerts))
|
||||
alertsRoute.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
|
||||
@@ -292,8 +344,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))
|
||||
@@ -320,8 +372,8 @@ func (hs *HttpServer) registerRoutes() {
|
||||
r.Any("/api/gnet/*", reqSignedIn, ProxyGnetRequest)
|
||||
|
||||
// Gravatar service.
|
||||
avt := avatar.CacheServer()
|
||||
r.Get("/avatar/:hash", avt.ServeHTTP)
|
||||
avatarCacheServer := avatar.NewCacheServer()
|
||||
r.Get("/avatar/:hash", avatarCacheServer.Handler)
|
||||
|
||||
// Websocket
|
||||
r.Any("/ws", hs.streamManager.Serve)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -24,6 +24,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var gravatarSource string
|
||||
@@ -89,12 +92,12 @@ func (this *Avatar) Update() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
type service struct {
|
||||
type CacheServer struct {
|
||||
notFound *Avatar
|
||||
cache map[string]*Avatar
|
||||
cache *gocache.Cache
|
||||
}
|
||||
|
||||
func (this *service) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
|
||||
func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
|
||||
for _, k := range keys {
|
||||
if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil {
|
||||
defaultValue = v
|
||||
@@ -103,13 +106,15 @@ func (this *service) mustInt(r *http.Request, defaultValue int, keys ...string)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
urlPath := r.URL.Path
|
||||
func (this *CacheServer) Handler(ctx *macaron.Context) {
|
||||
urlPath := ctx.Req.URL.Path
|
||||
hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
|
||||
|
||||
var avatar *Avatar
|
||||
|
||||
if avatar, _ = this.cache[hash]; avatar == nil {
|
||||
if obj, exist := this.cache.Get(hash); exist {
|
||||
avatar = obj.(*Avatar)
|
||||
} else {
|
||||
avatar = New(hash)
|
||||
}
|
||||
|
||||
@@ -123,36 +128,40 @@ func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if avatar.notFound {
|
||||
avatar = this.notFound
|
||||
} else {
|
||||
this.cache[hash] = avatar
|
||||
this.cache.Add(hash, avatar, gocache.DefaultExpiration)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(avatar.data.Bytes())))
|
||||
w.Header().Set("Cache-Control", "private, max-age=3600")
|
||||
ctx.Resp.Header().Add("Content-Type", "image/jpeg")
|
||||
|
||||
if err := avatar.Encode(w); err != nil {
|
||||
if !setting.EnableGzip {
|
||||
ctx.Resp.Header().Add("Content-Length", strconv.Itoa(len(avatar.data.Bytes())))
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Add("Cache-Control", "private, max-age=3600")
|
||||
|
||||
if err := avatar.Encode(ctx.Resp); err != nil {
|
||||
log.Warn("avatar encode error: %v", err)
|
||||
w.WriteHeader(500)
|
||||
ctx.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
|
||||
func CacheServer() http.Handler {
|
||||
func NewCacheServer() *CacheServer {
|
||||
UpdateGravatarSource()
|
||||
|
||||
return &service{
|
||||
return &CacheServer{
|
||||
notFound: newNotFound(),
|
||||
cache: make(map[string]*Avatar),
|
||||
cache: gocache.New(time.Hour, time.Hour*2),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
105
pkg/api/common_test.go
Normal 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
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -13,16 +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/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"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
|
||||
}
|
||||
@@ -35,23 +35,33 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func GetDashboard(c *middleware.Context) {
|
||||
slug := strings.ToLower(c.Params(":slug"))
|
||||
|
||||
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
|
||||
err := bus.Dispatch(&query)
|
||||
func dashboardGuardianResponse(err error) Response {
|
||||
if err != nil {
|
||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
||||
return
|
||||
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||
}
|
||||
|
||||
isStarred, err := isDashboardStarredByUser(c, query.Result.Id)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err)
|
||||
return
|
||||
return ApiError(403, "Access denied to this dashboard", nil)
|
||||
}
|
||||
|
||||
func GetDashboard(c *m.ReqContext) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
dash := query.Result
|
||||
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
|
||||
if canView, err := guardian.CanView(); err != nil || !canView {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
canEdit, _ := guardian.CanEdit()
|
||||
canSave, _ := guardian.CanSave()
|
||||
canAdmin, _ := guardian.CanAdmin()
|
||||
|
||||
isStarred, err := isDashboardStarredByUser(c, dash.Id)
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking if dashboard was starred by user", err)
|
||||
}
|
||||
|
||||
// Finding creator and last updater of the dashboard
|
||||
updater, creator := "Anonymous", "Anonymous"
|
||||
@@ -62,29 +72,46 @@ func GetDashboard(c *middleware.Context) {
|
||||
creator = getUserLogin(dash.CreatedBy)
|
||||
}
|
||||
|
||||
meta := dtos.DashboardMeta{
|
||||
IsStarred: isStarred,
|
||||
Slug: dash.Slug,
|
||||
Type: m.DashTypeDB,
|
||||
CanStar: c.IsSignedIn,
|
||||
CanSave: canSave,
|
||||
CanEdit: canEdit,
|
||||
CanAdmin: canAdmin,
|
||||
Created: dash.Created,
|
||||
Updated: dash.Updated,
|
||||
UpdatedBy: updater,
|
||||
CreatedBy: creator,
|
||||
Version: dash.Version,
|
||||
HasAcl: dash.HasAcl,
|
||||
IsFolder: dash.IsFolder,
|
||||
FolderId: dash.FolderId,
|
||||
Url: dash.GetUrl(),
|
||||
FolderTitle: "General",
|
||||
}
|
||||
|
||||
// lookup folder title
|
||||
if dash.FolderId > 0 {
|
||||
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
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
|
||||
dash.Data.Set("version", dash.Version)
|
||||
|
||||
dto := dtos.DashboardFullWithMeta{
|
||||
Dashboard: dash.Data,
|
||||
Meta: dtos.DashboardMeta{
|
||||
IsStarred: isStarred,
|
||||
Slug: slug,
|
||||
Type: m.DashTypeDB,
|
||||
CanStar: c.IsSignedIn,
|
||||
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
|
||||
CanEdit: canEditDashboard(c.OrgRole),
|
||||
Created: dash.Created,
|
||||
Updated: dash.Updated,
|
||||
UpdatedBy: updater,
|
||||
CreatedBy: creator,
|
||||
Version: dash.Version,
|
||||
},
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
// TODO(ben): copy this performance metrics logic for the new API endpoints added
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Get)
|
||||
c.JSON(200, dto)
|
||||
return Json(200, dto)
|
||||
}
|
||||
|
||||
func getUserLogin(userId int64) string {
|
||||
@@ -98,39 +125,84 @@ func getUserLogin(userId int64) string {
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteDashboard(c *middleware.Context) {
|
||||
slug := c.Params(":slug")
|
||||
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}
|
||||
}
|
||||
|
||||
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
||||
return
|
||||
return nil, ApiError(404, "Dashboard not found", err)
|
||||
}
|
||||
|
||||
cmd := m.DeleteDashboardCommand{Slug: slug, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
c.JsonApiErr(500, "Failed to delete dashboard", err)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = map[string]interface{}{"title": query.Result.Title}
|
||||
|
||||
c.JSON(200, resp)
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
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.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 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()
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -139,19 +211,39 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
}
|
||||
}
|
||||
|
||||
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
dashItem := &dashboards.SaveDashboardDTO{
|
||||
Dashboard: dash,
|
||||
Message: cmd.Message,
|
||||
OrgId: c.OrgId,
|
||||
User: c.SignedInUser,
|
||||
Overwrite: cmd.Overwrite,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||
dashboard, err := dashboards.NewService().SaveDashboard(dashItem)
|
||||
|
||||
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 {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&cmd)
|
||||
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 {
|
||||
@@ -171,35 +263,33 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
return ApiError(500, "Failed to save dashboard", err)
|
||||
}
|
||||
|
||||
alertCmd := alerting.UpdateDashboardAlertsCommand{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Dashboard: cmd.Result,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
return ApiError(500, "Failed to save alerts", err)
|
||||
if err == m.ErrDashboardFailedToUpdateAlertData {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
|
||||
return Json(200, util.DynMap{
|
||||
"status": "success",
|
||||
"slug": dashboard.Slug,
|
||||
"version": dashboard.Version,
|
||||
"id": dashboard.Id,
|
||||
"uid": dashboard.Uid,
|
||||
"url": dashboard.GetUrl(),
|
||||
})
|
||||
}
|
||||
|
||||
func canEditDashboard(role m.RoleType) bool {
|
||||
return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -214,7 +304,9 @@ func GetHomeDashboard(c *middleware.Context) Response {
|
||||
|
||||
dash := dtos.DashboardFullWithMeta{}
|
||||
dash.Meta.IsHome = true
|
||||
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
|
||||
dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
|
||||
dash.Meta.FolderTitle = "General"
|
||||
|
||||
jsonParser := json.NewDecoder(file)
|
||||
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
|
||||
return ApiError(500, "Failed to load home dashboard", err)
|
||||
@@ -228,55 +320,41 @@ func GetHomeDashboard(c *middleware.Context) Response {
|
||||
}
|
||||
|
||||
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
||||
rows := dash.Get("rows").MustArray()
|
||||
row := simplejson.NewFromAny(rows[0])
|
||||
panels := dash.Get("panels").MustArray()
|
||||
|
||||
newpanel := simplejson.NewFromAny(map[string]interface{}{
|
||||
"type": "gettingstarted",
|
||||
"id": 123123,
|
||||
"span": 12,
|
||||
"gridPos": map[string]interface{}{
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"w": 24,
|
||||
"h": 4,
|
||||
},
|
||||
})
|
||||
|
||||
panels := row.Get("panels").MustArray()
|
||||
panels = append(panels, newpanel)
|
||||
row.Set("panels", panels)
|
||||
}
|
||||
|
||||
func GetDashboardFromJsonFile(c *middleware.Context) {
|
||||
file := c.Params(":file")
|
||||
|
||||
dashboard := search.GetDashboardFromJsonIndex(file)
|
||||
if dashboard == nil {
|
||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
||||
return
|
||||
}
|
||||
|
||||
dash := dtos.DashboardFullWithMeta{Dashboard: dashboard.Data}
|
||||
dash.Meta.Type = m.DashTypeJson
|
||||
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
|
||||
|
||||
c.JSON(200, &dash)
|
||||
dash.Set("panels", panels)
|
||||
}
|
||||
|
||||
// GetDashboardVersions returns all dashboard versions as JSON
|
||||
func GetDashboardVersions(c *middleware.Context) Response {
|
||||
dashboardId := c.ParamsInt64(":dashboardId")
|
||||
limit := c.QueryInt("limit")
|
||||
start := c.QueryInt("start")
|
||||
func GetDashboardVersions(c *m.ReqContext) Response {
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
if limit == 0 {
|
||||
limit = 1000
|
||||
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
query := m.GetDashboardVersionsQuery{
|
||||
OrgId: c.OrgId,
|
||||
DashboardId: dashboardId,
|
||||
Limit: limit,
|
||||
Start: start,
|
||||
DashboardId: dashId,
|
||||
Limit: c.QueryInt("limit"),
|
||||
Start: c.QueryInt("start"),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashboardId), err)
|
||||
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err)
|
||||
}
|
||||
|
||||
for _, version := range query.Result {
|
||||
@@ -299,18 +377,22 @@ func GetDashboardVersions(c *middleware.Context) Response {
|
||||
}
|
||||
|
||||
// GetDashboardVersion returns the dashboard version with the given ID.
|
||||
func GetDashboardVersion(c *middleware.Context) Response {
|
||||
dashboardId := c.ParamsInt64(":dashboardId")
|
||||
version := c.ParamsInt(":id")
|
||||
func GetDashboardVersion(c *m.ReqContext) Response {
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
query := m.GetDashboardVersionQuery{
|
||||
OrgId: c.OrgId,
|
||||
DashboardId: dashboardId,
|
||||
Version: version,
|
||||
DashboardId: dashId,
|
||||
Version: c.ParamsInt(":id"),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", version, dashboardId), err)
|
||||
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err)
|
||||
}
|
||||
|
||||
creator := "Anonymous"
|
||||
@@ -327,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,
|
||||
@@ -354,26 +448,28 @@ 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 {
|
||||
dashboardId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
dashQuery := m.GetDashboardQuery{Id: dashboardId, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&dashQuery); err != nil {
|
||||
return ApiError(404, "Dashboard not found", nil)
|
||||
func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
versionQuery := m.GetDashboardVersionQuery{DashboardId: dashboardId, Version: apiCmd.Version, OrgId: c.OrgId}
|
||||
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&versionQuery); err != nil {
|
||||
return ApiError(404, "Dashboard version not found", nil)
|
||||
}
|
||||
|
||||
dashboard := dashQuery.Result
|
||||
version := versionQuery.Result
|
||||
|
||||
saveCmd := m.SaveDashboardCommand{}
|
||||
@@ -381,13 +477,14 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
|
||||
saveCmd.OrgId = c.OrgId
|
||||
saveCmd.UserId = c.UserId
|
||||
saveCmd.Dashboard = version.Data
|
||||
saveCmd.Dashboard.Set("version", dashboard.Version)
|
||||
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 {
|
||||
|
||||
90
pkg/api/dashboard_permission.go
Normal file
90
pkg/api/dashboard_permission.go
Normal 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")
|
||||
}
|
||||
209
pkg/api/dashboard_permission_test.go
Normal file
209
pkg/api/dashboard_permission_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
97
pkg/api/dashboard_snapshot_test.go
Normal file
97
pkg/api/dashboard_snapshot_test.go
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
911
pkg/api/dashboard_test.go
Normal file
911
pkg/api/dashboard_test.go
Normal file
@@ -0,0 +1,911 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/setting"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
// 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() {
|
||||
fakeDash := m.NewDashboard("Child dash")
|
||||
fakeDash.Id = 1
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
// 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/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)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
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) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(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/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)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
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) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a dashboard with a parent folder which has an acl", t, func() {
|
||||
fakeDash := m.NewDashboard("Child dash")
|
||||
fakeDash.Id = 1
|
||||
fakeDash.FolderId = 1
|
||||
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,
|
||||
Permission: m.PERMISSION_EDIT,
|
||||
UserId: 200,
|
||||
},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = aclMockResp
|
||||
return nil
|
||||
})
|
||||
|
||||
var getDashboardQueries []*m.GetDashboardQuery
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = fakeDash
|
||||
getDashboardQueries = append(getDashboardQueries, query)
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = []*m.Team{}
|
||||
return nil
|
||||
})
|
||||
|
||||
// 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/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 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) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(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/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 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) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(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{
|
||||
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
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)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
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) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Viewer and viewers can edit", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
setting.ViewersCanEdit = true
|
||||
|
||||
mockResult := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
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)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is an Org Viewer but has an admin permission", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
mockResult := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
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)
|
||||
So(dash.Meta.CanAdmin, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
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) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(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{
|
||||
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = mockResult
|
||||
return nil
|
||||
})
|
||||
|
||||
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 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) {
|
||||
CallGetDashboardVersion(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||
CallGetDashboardVersions(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 {
|
||||
CallGetDashboard(sc)
|
||||
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
dash := dtos.DashboardFullWithMeta{}
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&dash)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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{}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = GetDashboardVersion
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallGetDashboardVersions(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(query *m.GetDashboardVersionsQuery) error {
|
||||
query.Result = []*m.DashboardVersionDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = GetDashboardVersions
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallDeleteDashboard(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = DeleteDashboard
|
||||
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) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
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 := setupScenarioContext(url)
|
||||
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
|
||||
sc.context = c
|
||||
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
|
||||
|
||||
return PostDashboard(c, cmd)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
@@ -33,6 +32,7 @@ func GetDataSources(c *middleware.Context) Response {
|
||||
BasicAuth: ds.BasicAuth,
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: ds.JsonData,
|
||||
ReadOnly: ds.ReadOnly,
|
||||
}
|
||||
|
||||
if plugin, exists := plugins.DataSources[ds.Type]; exists {
|
||||
@@ -49,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,
|
||||
@@ -68,61 +68,78 @@ func GetDataSourceById(c *middleware.Context) Response {
|
||||
return Json(200, &dtos)
|
||||
}
|
||||
|
||||
func DeleteDataSourceById(c *middleware.Context) {
|
||||
func DeleteDataSourceById(c *m.ReqContext) Response {
|
||||
id := c.ParamsInt64(":id")
|
||||
|
||||
if id <= 0 {
|
||||
c.JsonApiErr(400, "Missing valid datasource id", nil)
|
||||
return
|
||||
return ApiError(400, "Missing valid datasource id", nil)
|
||||
}
|
||||
|
||||
ds, err := getRawDataSourceById(id, c.OrgId)
|
||||
if err != nil {
|
||||
return ApiError(400, "Failed to delete datasource", nil)
|
||||
}
|
||||
|
||||
if ds.ReadOnly {
|
||||
return ApiError(403, "Cannot delete read-only data source", nil)
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
err = bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to delete datasource", err)
|
||||
return
|
||||
return ApiError(500, "Failed to delete datasource", err)
|
||||
}
|
||||
|
||||
c.JsonOK("Data source deleted")
|
||||
return ApiSuccess("Data source deleted")
|
||||
}
|
||||
|
||||
func DeleteDataSourceByName(c *middleware.Context) {
|
||||
func DeleteDataSourceByName(c *m.ReqContext) Response {
|
||||
name := c.Params(":name")
|
||||
|
||||
if name == "" {
|
||||
c.JsonApiErr(400, "Missing valid datasource name", nil)
|
||||
return
|
||||
return ApiError(400, "Missing valid datasource name", nil)
|
||||
}
|
||||
|
||||
getCmd := &m.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(getCmd); err != nil {
|
||||
return ApiError(500, "Failed to delete datasource", err)
|
||||
}
|
||||
|
||||
if getCmd.Result.ReadOnly {
|
||||
return ApiError(403, "Cannot delete read-only data source", nil)
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to delete datasource", err)
|
||||
return
|
||||
return ApiError(500, "Failed to delete datasource", err)
|
||||
}
|
||||
|
||||
c.JsonOK("Data source deleted")
|
||||
return ApiSuccess("Data source deleted")
|
||||
}
|
||||
|
||||
func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
|
||||
func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrDataSourceNameExists {
|
||||
c.JsonApiErr(409, err.Error(), err)
|
||||
return
|
||||
return ApiError(409, err.Error(), err)
|
||||
}
|
||||
|
||||
c.JsonApiErr(500, "Failed to add datasource", err)
|
||||
return
|
||||
return ApiError(500, "Failed to add datasource", err)
|
||||
}
|
||||
|
||||
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name})
|
||||
ds := convertModelToDtos(cmd.Result)
|
||||
return Json(200, util.DynMap{
|
||||
"message": "Datasource added",
|
||||
"id": cmd.Result.Id,
|
||||
"name": cmd.Result.Name,
|
||||
"datasource": ds,
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@ -133,10 +150,19 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
|
||||
|
||||
err = bus.Dispatch(&cmd)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to update datasource", err)
|
||||
if err == m.ErrDataSourceUpdatingOldVersion {
|
||||
return ApiError(500, "Failed to update datasource. Reload new version and try again", err)
|
||||
} else {
|
||||
return ApiError(500, "Failed to update datasource", err)
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{"message": "Datasource updated", "id": cmd.Id, "name": cmd.Name})
|
||||
ds := convertModelToDtos(cmd.Result)
|
||||
return Json(200, util.DynMap{
|
||||
"message": "Datasource updated",
|
||||
"id": cmd.Id,
|
||||
"name": cmd.Name,
|
||||
"datasource": ds,
|
||||
})
|
||||
}
|
||||
|
||||
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
|
||||
@@ -145,11 +171,14 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
|
||||
}
|
||||
|
||||
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ds.ReadOnly {
|
||||
return m.ErrDatasourceIsReadOnly
|
||||
}
|
||||
|
||||
secureJsonData := ds.SecureJsonData.Decrypt()
|
||||
for k, v := range secureJsonData {
|
||||
|
||||
@@ -158,8 +187,6 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
|
||||
}
|
||||
}
|
||||
|
||||
// set version from db
|
||||
cmd.Version = ds.Version
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -177,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 {
|
||||
@@ -188,11 +215,12 @@ func GetDataSourceByName(c *middleware.Context) Response {
|
||||
}
|
||||
|
||||
dtos := convertModelToDtos(query.Result)
|
||||
dtos.ReadOnly = true
|
||||
return Json(200, &dtos)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -228,6 +256,8 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: ds.JsonData,
|
||||
SecureJsonFields: map[string]bool{},
|
||||
Version: ds.Version,
|
||||
ReadOnly: ds.ReadOnly,
|
||||
}
|
||||
|
||||
for k, v := range ds.SecureJsonData {
|
||||
|
||||
@@ -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,79 +48,3 @@ func TestDataSourcesProxy(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func loggedInUserScenario(desc string, url string, 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 = models.ROLE_EDITOR
|
||||
if sc.handlerFunc != nil {
|
||||
return sc.handlerFunc(sc.context)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get(url, 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
|
||||
|
||||
16
pkg/api/dtos/acl.go
Normal file
16
pkg/api/dtos/acl.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package dtos
|
||||
|
||||
import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type UpdateDashboardAclCommand struct {
|
||||
Items []DashboardAclUpdateItem `json:"items"`
|
||||
}
|
||||
|
||||
type DashboardAclUpdateItem struct {
|
||||
UserId int64 `json:"userId"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
Role *m.RoleType `json:"role,omitempty"`
|
||||
Permission m.PermissionType `json:"permission"`
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -7,20 +7,27 @@ import (
|
||||
)
|
||||
|
||||
type DashboardMeta struct {
|
||||
IsStarred bool `json:"isStarred,omitempty"`
|
||||
IsHome bool `json:"isHome,omitempty"`
|
||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanStar bool `json:"canStar"`
|
||||
Slug string `json:"slug"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Version int `json:"version"`
|
||||
IsStarred bool `json:"isStarred,omitempty"`
|
||||
IsHome bool `json:"isHome,omitempty"`
|
||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
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"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Version int `json:"version"`
|
||||
HasAcl bool `json:"hasAcl"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderUrl string `json:"folderUrl"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
||||
61
pkg/api/dtos/datasource.go
Normal file
61
pkg/api/dtos/datasource.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package dtos
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type DataSource struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
TypeLogoUrl string `json:"typeLogoUrl"`
|
||||
Access m.DsAccess `json:"access"`
|
||||
Url string `json:"url"`
|
||||
Password string `json:"password"`
|
||||
User string `json:"user"`
|
||||
Database string `json:"database"`
|
||||
BasicAuth bool `json:"basicAuth"`
|
||||
BasicAuthUser string `json:"basicAuthUser"`
|
||||
BasicAuthPassword string `json:"basicAuthPassword"`
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData *simplejson.Json `json:"jsonData,omitempty"`
|
||||
SecureJsonFields map[string]bool `json:"secureJsonFields"`
|
||||
Version int `json:"version"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
}
|
||||
|
||||
type DataSourceListItemDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
TypeLogoUrl string `json:"typeLogoUrl"`
|
||||
Access m.DsAccess `json:"access"`
|
||||
Url string `json:"url"`
|
||||
Password string `json:"password"`
|
||||
User string `json:"user"`
|
||||
Database string `json:"database"`
|
||||
BasicAuth bool `json:"basicAuth"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData *simplejson.Json `json:"jsonData,omitempty"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
}
|
||||
|
||||
type DataSourceList []DataSourceListItemDTO
|
||||
|
||||
func (slice DataSourceList) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Less(i, j int) bool {
|
||||
return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
25
pkg/api/dtos/folder.go
Normal file
25
pkg/api/dtos/folder.go
Normal 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"`
|
||||
}
|
||||
@@ -7,9 +7,10 @@ type IndexViewData struct {
|
||||
AppSubUrl string
|
||||
GoogleAnalyticsId string
|
||||
GoogleTagManagerId string
|
||||
MainNavLinks []*NavLink
|
||||
NavTree []*NavLink
|
||||
BuildVersion string
|
||||
BuildCommit string
|
||||
Theme string
|
||||
NewGrafanaVersionExists bool
|
||||
NewGrafanaVersion string
|
||||
}
|
||||
@@ -20,10 +21,16 @@ type PluginCss struct {
|
||||
}
|
||||
|
||||
type NavLink struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Img string `json:"img,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Divider bool `json:"divider,omitempty"`
|
||||
Children []*NavLink `json:"children,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SubTitle string `json:"subTitle,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Img string `json:"img,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Divider bool `json:"divider,omitempty"`
|
||||
HideFromMenu bool `json:"hideFromMenu,omitempty"`
|
||||
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||
Children []*NavLink `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ type AddInviteForm struct {
|
||||
LoginOrEmail string `json:"loginOrEmail" binding:"Required"`
|
||||
Name string `json:"name"`
|
||||
Role m.RoleType `json:"role" binding:"Required"`
|
||||
SkipEmails bool `json:"skipEmails"`
|
||||
SendEmail bool `json:"sendEmail"`
|
||||
}
|
||||
|
||||
type InviteInfo struct {
|
||||
|
||||
@@ -3,6 +3,7 @@ package dtos
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -27,6 +28,7 @@ type CurrentUser struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
LightTheme bool `json:"lightTheme"`
|
||||
OrgCount int `json:"orgCount"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
OrgName string `json:"orgName"`
|
||||
OrgRole m.RoleType `json:"orgRole"`
|
||||
@@ -37,56 +39,6 @@ type CurrentUser struct {
|
||||
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
|
||||
}
|
||||
|
||||
type DataSource struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
TypeLogoUrl string `json:"typeLogoUrl"`
|
||||
Access m.DsAccess `json:"access"`
|
||||
Url string `json:"url"`
|
||||
Password string `json:"password"`
|
||||
User string `json:"user"`
|
||||
Database string `json:"database"`
|
||||
BasicAuth bool `json:"basicAuth"`
|
||||
BasicAuthUser string `json:"basicAuthUser"`
|
||||
BasicAuthPassword string `json:"basicAuthPassword"`
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData *simplejson.Json `json:"jsonData,omitempty"`
|
||||
SecureJsonFields map[string]bool `json:"secureJsonFields"`
|
||||
}
|
||||
|
||||
type DataSourceListItemDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
TypeLogoUrl string `json:"typeLogoUrl"`
|
||||
Access m.DsAccess `json:"access"`
|
||||
Url string `json:"url"`
|
||||
Password string `json:"password"`
|
||||
User string `json:"user"`
|
||||
Database string `json:"database"`
|
||||
BasicAuth bool `json:"basicAuth"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData *simplejson.Json `json:"jsonData,omitempty"`
|
||||
}
|
||||
|
||||
type DataSourceList []DataSourceListItemDTO
|
||||
|
||||
func (slice DataSourceList) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Less(i, j int) bool {
|
||||
return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
|
||||
type MetricRequest struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
@@ -106,3 +58,19 @@ func GetGravatarUrl(text string) string {
|
||||
hasher.Write([]byte(strings.ToLower(text)))
|
||||
return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func GetGravatarUrlWithDefault(text string, defaultText string) string {
|
||||
if text != "" {
|
||||
return GetGravatarUrl(text)
|
||||
}
|
||||
|
||||
reg, err := regexp.Compile("[^a-zA-Z0-9]+")
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
text = reg.ReplaceAllString(defaultText, "") + "@localhost"
|
||||
|
||||
return GetGravatarUrl(text)
|
||||
}
|
||||
|
||||
146
pkg/api/folder.go
Normal file
146
pkg/api/folder.go
Normal 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)
|
||||
}
|
||||
107
pkg/api/folder_permission.go
Normal file
107
pkg/api/folder_permission.go
Normal 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")
|
||||
}
|
||||
241
pkg/api/folder_permission_test.go
Normal file
241
pkg/api/folder_permission_test.go
Normal 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
251
pkg/api/folder_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -143,7 +142,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||
"disableLoginForm": setting.DisableLoginForm,
|
||||
"disableSignoutMenu": setting.DisableSignoutMenu,
|
||||
"externalUserMngInfo": setting.ExternalUserMngInfo,
|
||||
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
||||
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
||||
@@ -181,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,7 +95,7 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
|
||||
func (hs *HttpServer) Shutdown(ctx context.Context) error {
|
||||
err := hs.httpSrv.Shutdown(ctx)
|
||||
hs.log.Info("stopped http server")
|
||||
hs.log.Info("Stopped HTTP server")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -146,12 +146,13 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
m := macaron.New()
|
||||
|
||||
m.Use(middleware.Logger())
|
||||
m.Use(middleware.Recovery())
|
||||
|
||||
if setting.EnableGzip {
|
||||
m.Use(middleware.Gziper())
|
||||
}
|
||||
|
||||
m.Use(middleware.Recovery())
|
||||
|
||||
for _, route := range plugins.StaticRoutes {
|
||||
pluginRoute := path.Join("/public/plugins/", route.PluginId)
|
||||
hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
|
||||
@@ -161,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,
|
||||
@@ -188,13 +193,13 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
|
||||
DisableCompression: true,
|
||||
}).ServeHTTP(ctx.Resp, ctx.Req.Request)
|
||||
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
|
||||
ServeHTTP(ctx.Resp, ctx.Req.Request)
|
||||
}
|
||||
|
||||
func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
|
||||
if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/api/health" {
|
||||
notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
|
||||
if notHeadOrGet || ctx.Req.URL.Path != "/api/health" {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
200
pkg/api/index.go
200
pkg/api/index.go
@@ -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
|
||||
@@ -50,6 +49,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
Login: c.Login,
|
||||
Email: c.Email,
|
||||
Name: c.Name,
|
||||
OrgCount: c.OrgCount,
|
||||
OrgId: c.OrgId,
|
||||
OrgName: c.OrgName,
|
||||
OrgRole: c.OrgRole,
|
||||
@@ -61,6 +61,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
HelpFlags1: c.HelpFlags1,
|
||||
},
|
||||
Settings: settings,
|
||||
Theme: prefs.Theme,
|
||||
AppUrl: appUrl,
|
||||
AppSubUrl: appSubUrl,
|
||||
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
||||
@@ -72,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 {
|
||||
@@ -82,52 +83,77 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
themeUrlParam := c.Query("theme")
|
||||
if themeUrlParam == "light" {
|
||||
data.User.LightTheme = true
|
||||
}
|
||||
|
||||
dashboardChildNavs := []*dtos.NavLink{
|
||||
{Text: "Home", Url: setting.AppSubUrl + "/"},
|
||||
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists"},
|
||||
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"},
|
||||
data.Theme = "light"
|
||||
}
|
||||
|
||||
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"})
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Create",
|
||||
Id: "create",
|
||||
Icon: "fa fa-fw fa-plus",
|
||||
Url: setting.AppSubUrl + "/dashboard/new",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
|
||||
{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"},
|
||||
{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
dashboardChildNavs := []*dtos.NavLink{
|
||||
{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"},
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Dashboards",
|
||||
Icon: "icon-gf icon-gf-dashboard",
|
||||
Id: "dashboards",
|
||||
SubTitle: "Manage dashboards & folders",
|
||||
Icon: "gicon gicon-dashboard",
|
||||
Url: setting.AppSubUrl + "/",
|
||||
Children: dashboardChildNavs,
|
||||
})
|
||||
|
||||
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
||||
alertChildNavs := []*dtos.NavLink{
|
||||
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"},
|
||||
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"},
|
||||
if c.IsSignedIn {
|
||||
profileNode := &dtos.NavLink{
|
||||
Text: c.SignedInUser.NameOrFallback(),
|
||||
SubTitle: c.SignedInUser.Login,
|
||||
Id: "profile",
|
||||
Img: data.User.GravatarUrl,
|
||||
Url: setting.AppSubUrl + "/profile",
|
||||
HideFromMenu: true,
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Preferences", Id: "profile-settings", Url: setting.AppSubUrl + "/profile", Icon: "gicon gicon-preferences"},
|
||||
{Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true},
|
||||
},
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Alerting",
|
||||
Icon: "icon-gf icon-gf-alert",
|
||||
Url: setting.AppSubUrl + "/alerting/list",
|
||||
Children: alertChildNavs,
|
||||
})
|
||||
if !setting.DisableSignoutMenu {
|
||||
// add sign out first
|
||||
profileNode.Children = append(profileNode.Children, &dtos.NavLink{
|
||||
Text: "Sign out", Id: "sign-out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self",
|
||||
})
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, profileNode)
|
||||
}
|
||||
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Data Sources",
|
||||
Icon: "icon-gf icon-gf-datasources",
|
||||
Url: setting.AppSubUrl + "/datasources",
|
||||
})
|
||||
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
||||
alertChildNavs := []*dtos.NavLink{
|
||||
{Text: "Alert Rules", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "gicon gicon-alert-rules"},
|
||||
{Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "gicon gicon-alert-notification-channel"},
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Plugins",
|
||||
Icon: "icon-gf icon-gf-apps",
|
||||
Url: setting.AppSubUrl + "/plugins",
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Alerting",
|
||||
SubTitle: "Alert rules & notifications",
|
||||
Id: "alerting",
|
||||
Icon: "gicon gicon-alert",
|
||||
Url: setting.AppSubUrl + "/alerting/list",
|
||||
Children: alertChildNavs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,6 +166,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
if plugin.Pinned {
|
||||
appLink := &dtos.NavLink{
|
||||
Text: plugin.Name,
|
||||
Id: "plugin-page-" + plugin.Id,
|
||||
Url: plugin.DefaultNavUrl,
|
||||
Img: plugin.Info.Logos.Small,
|
||||
}
|
||||
@@ -168,33 +195,110 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
|
||||
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
}
|
||||
|
||||
if len(appLink.Children) > 0 {
|
||||
data.MainNavLinks = append(data.MainNavLinks, appLink)
|
||||
data.NavTree = append(data.NavTree, appLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.IsGrafanaAdmin {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Admin",
|
||||
Icon: "fa fa-fw fa-cogs",
|
||||
Url: setting.AppSubUrl + "/admin",
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
cfgNode := &dtos.NavLink{
|
||||
Id: "cfg",
|
||||
Text: "Configuration",
|
||||
SubTitle: "Organization: " + c.OrgName,
|
||||
Icon: "gicon gicon-cog",
|
||||
Url: setting.AppSubUrl + "/datasources",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"},
|
||||
{Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"},
|
||||
{Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"},
|
||||
{Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"},
|
||||
{
|
||||
Text: "Data Sources",
|
||||
Icon: "gicon gicon-datasources",
|
||||
Description: "Add and configure data sources",
|
||||
Id: "datasources",
|
||||
Url: setting.AppSubUrl + "/datasources",
|
||||
},
|
||||
{
|
||||
Text: "Users",
|
||||
Id: "users",
|
||||
Description: "Manage org members",
|
||||
Icon: "gicon gicon-user",
|
||||
Url: setting.AppSubUrl + "/org/users",
|
||||
},
|
||||
{
|
||||
Text: "Teams",
|
||||
Id: "teams",
|
||||
Description: "Manage org groups",
|
||||
Icon: "gicon gicon-team",
|
||||
Url: setting.AppSubUrl + "/org/teams",
|
||||
},
|
||||
{
|
||||
Text: "Plugins",
|
||||
Id: "plugins",
|
||||
Description: "View and configure plugins",
|
||||
Icon: "gicon gicon-plugins",
|
||||
Url: setting.AppSubUrl + "/plugins",
|
||||
},
|
||||
{
|
||||
Text: "Preferences",
|
||||
Id: "org-settings",
|
||||
Description: "Organization preferences",
|
||||
Icon: "gicon gicon-preferences",
|
||||
Url: setting.AppSubUrl + "/org",
|
||||
},
|
||||
|
||||
{
|
||||
Text: "API Keys",
|
||||
Id: "apikeys",
|
||||
Description: "Create & manage API keys",
|
||||
Icon: "gicon gicon-apikeys",
|
||||
Url: setting.AppSubUrl + "/org/apikeys",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if c.IsGrafanaAdmin {
|
||||
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||
Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
|
||||
})
|
||||
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||
Text: "Server Admin",
|
||||
HideFromTabs: true,
|
||||
SubTitle: "Manage all users & orgs",
|
||||
Id: "admin",
|
||||
Icon: "gicon gicon-shield",
|
||||
Url: setting.AppSubUrl + "/admin/users",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
|
||||
{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
|
||||
{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
|
||||
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
|
||||
{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, cfgNode)
|
||||
}
|
||||
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Help",
|
||||
Id: "help",
|
||||
Url: "#",
|
||||
Icon: "gicon gicon-question",
|
||||
HideFromMenu: true,
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"},
|
||||
{Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"},
|
||||
{Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"},
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
@@ -203,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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -61,7 +60,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
|
||||
}
|
||||
|
||||
// send invite email
|
||||
if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) {
|
||||
if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
|
||||
emailCmd := m.SendEmailCommand{
|
||||
To: []string{inviteDto.LoginOrEmail},
|
||||
Template: "new_user_invite.html",
|
||||
@@ -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 {
|
||||
@@ -99,7 +98,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto
|
||||
return ApiError(500, "Error while trying to create org user", err)
|
||||
} else {
|
||||
|
||||
if !inviteDto.SkipEmails && util.IsEmail(user.Email) {
|
||||
if inviteDto.SendEmail && util.IsEmail(user.Email) {
|
||||
emailCmd := m.SendEmailCommand{
|
||||
To: []string{user.Email},
|
||||
Template: "invited_to_org.html",
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
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)
|
||||
}
|
||||
@@ -31,10 +31,6 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
|
||||
|
||||
userToAdd := userQuery.Result
|
||||
|
||||
// if userToAdd.Id == c.UserId {
|
||||
// return ApiError(400, "Cannot add yourself as user", nil)
|
||||
// }
|
||||
|
||||
cmd.UserId = userToAdd.Id
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
@@ -48,34 +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)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
for _, user := range q.Result {
|
||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -97,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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,10 +126,10 @@ 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.UserId, playlistId)
|
||||
playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId)
|
||||
if err != nil {
|
||||
return ApiError(500, "Could not load dashboards", err)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,18 +34,18 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
|
||||
func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
|
||||
result := make(dtos.PlaylistDashboardsSlice, 0)
|
||||
|
||||
if len(dashboardByTag) > 0 {
|
||||
for _, tag := range dashboardByTag {
|
||||
searchQuery := search.Query{
|
||||
Title: "",
|
||||
Tags: []string{tag},
|
||||
UserId: userId,
|
||||
Limit: 100,
|
||||
IsStarred: false,
|
||||
OrgId: orgId,
|
||||
Title: "",
|
||||
Tags: []string{tag},
|
||||
SignedInUser: signedInUser,
|
||||
Limit: 100,
|
||||
IsStarred: false,
|
||||
OrgId: orgId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&searchQuery); err == nil {
|
||||
@@ -64,7 +64,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
|
||||
return result
|
||||
}
|
||||
|
||||
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
|
||||
func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
|
||||
playlistItems, _ := LoadPlaylistItems(playlistId)
|
||||
|
||||
dashboardByIds := make([]int64, 0)
|
||||
@@ -89,7 +89,7 @@ func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashb
|
||||
|
||||
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
|
||||
result = append(result, k...)
|
||||
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
|
||||
result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...)
|
||||
|
||||
sort.Sort(result)
|
||||
return result, nil
|
||||
|
||||
@@ -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{
|
||||
@@ -135,9 +134,24 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
req.Header.Add("Authorization", dsAuth)
|
||||
}
|
||||
|
||||
// clear cookie headers
|
||||
// clear cookie header, except for whitelisted cookies
|
||||
var keptCookies []*http.Cookie
|
||||
if proxy.ds.JsonData != nil {
|
||||
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
|
||||
keepCookieNames := keepCookies.MustStringArray()
|
||||
for _, c := range req.Cookies() {
|
||||
for _, v := range keepCookieNames {
|
||||
if c.Name == v {
|
||||
keptCookies = append(keptCookies, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
for _, c := range keptCookies {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
|
||||
// clear X-Forwarded Host/Port/Proto headers
|
||||
req.Header.Del("X-Forwarded-Host")
|
||||
@@ -177,8 +191,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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +262,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)
|
||||
|
||||
@@ -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")
|
||||
@@ -149,6 +148,58 @@ func TestDSRouteRule(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying a data source with no keepCookies specified", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
json, _ := simplejson.NewJson([]byte(`{"keepCookies": []}`))
|
||||
|
||||
ds := &m.DataSource{
|
||||
Type: m.DS_GRAPHITE,
|
||||
Url: "http://graphite:8086",
|
||||
JsonData: json,
|
||||
}
|
||||
|
||||
ctx := &m.ReqContext{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Should clear all cookies", func() {
|
||||
So(req.Header.Get("Cookie"), ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying a data source with keep cookies specified", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
json, _ := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`))
|
||||
|
||||
ds := &m.DataSource{
|
||||
Type: m.DS_GRAPHITE,
|
||||
Url: "http://graphite:8086",
|
||||
JsonData: json,
|
||||
}
|
||||
|
||||
ctx := &m.ReqContext{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Should keep named cookies", func() {
|
||||
So(req.Header.Get("Cookie"), ShouldEqual, "JSESSION_ID=test")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When interpolating string", func() {
|
||||
data := templateData{
|
||||
SecureJsonData: map[string]string{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -5,30 +5,38 @@ 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) {
|
||||
queryReader := util.NewUrlQueryReader(c.Req.URL)
|
||||
func RenderToPng(c *m.ReqContext) {
|
||||
queryReader, err := util.NewUrlQueryReader(c.Req.URL)
|
||||
if err != nil {
|
||||
c.Handle(400, "Render parameters error", err)
|
||||
return
|
||||
}
|
||||
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
|
||||
|
||||
renderOpts := &renderer.RenderOpts{
|
||||
Path: c.Params("*") + queryParams,
|
||||
Width: queryReader.Get("width", "800"),
|
||||
Height: queryReader.Get("height", "400"),
|
||||
OrgId: c.OrgId,
|
||||
Timeout: queryReader.Get("timeout", "60"),
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
OrgRole: c.OrgRole,
|
||||
Timezone: queryReader.Get("tz", ""),
|
||||
Encoding: queryReader.Get("encoding", ""),
|
||||
}
|
||||
|
||||
pngPath, err := renderer.RenderToPng(renderOpts)
|
||||
|
||||
if err != nil {
|
||||
if err == renderer.ErrTimeout {
|
||||
c.Handle(500, err.Error(), err)
|
||||
}
|
||||
if err != nil && err == renderer.ErrTimeout {
|
||||
c.Handle(500, err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.Handle(500, "Rendering failed.", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,8 +81,6 @@ func (rr *routeRegister) Register(router Router) *macaron.Router {
|
||||
}
|
||||
|
||||
func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {
|
||||
//inject tracing
|
||||
|
||||
h := make([]macaron.Handler, 0)
|
||||
for _, fn := range rr.namedMiddleware {
|
||||
h = append(h, fn(pattern))
|
||||
|
||||
@@ -5,36 +5,53 @@ 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
|
||||
}
|
||||
|
||||
dbids := make([]int, 0)
|
||||
if c.Query("permission") == "Edit" {
|
||||
permission = m.PERMISSION_EDIT
|
||||
}
|
||||
|
||||
dbids := make([]int64, 0)
|
||||
for _, id := range c.QueryStrings("dashboardIds") {
|
||||
dashboardId, err := strconv.Atoi(id)
|
||||
dashboardId, err := strconv.ParseInt(id, 10, 64)
|
||||
if err == nil {
|
||||
dbids = append(dbids, dashboardId)
|
||||
}
|
||||
}
|
||||
|
||||
folderIds := make([]int64, 0)
|
||||
for _, id := range c.QueryStrings("folderIds") {
|
||||
folderId, err := strconv.ParseInt(id, 10, 64)
|
||||
if err == nil {
|
||||
folderIds = append(folderIds, folderId)
|
||||
}
|
||||
}
|
||||
|
||||
searchQuery := search.Query{
|
||||
Title: query,
|
||||
Tags: tags,
|
||||
UserId: c.UserId,
|
||||
SignedInUser: c.SignedInUser,
|
||||
Limit: limit,
|
||||
IsStarred: starred == "true",
|
||||
OrgId: c.OrgId,
|
||||
DashboardIds: dbids,
|
||||
Type: dashboardType,
|
||||
FolderIds: folderIds,
|
||||
Permission: permission,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&searchQuery)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
|
||||
97
pkg/api/team.go
Normal file
97
pkg/api/team.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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/util"
|
||||
)
|
||||
|
||||
// POST /api/teams
|
||||
func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamNameTaken {
|
||||
return ApiError(409, "Team name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to create Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{
|
||||
"teamId": cmd.Result.Id,
|
||||
"message": "Team created",
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /api/teams/:teamId
|
||||
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 {
|
||||
return ApiError(400, "Team name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to update Team", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Team updated")
|
||||
}
|
||||
|
||||
// DELETE /api/teams/:teamId
|
||||
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)
|
||||
}
|
||||
return ApiError(500, "Failed to update Team", err)
|
||||
}
|
||||
return ApiSuccess("Team deleted")
|
||||
}
|
||||
|
||||
// GET /api/teams/search
|
||||
func SearchTeams(c *m.ReqContext) Response {
|
||||
perPage := c.QueryInt("perpage")
|
||||
if perPage <= 0 {
|
||||
perPage = 1000
|
||||
}
|
||||
page := c.QueryInt("page")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
query := m.SearchTeamsQuery{
|
||||
OrgId: c.OrgId,
|
||||
Query: c.Query("query"),
|
||||
Name: c.Query("name"),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to search Teams", err)
|
||||
}
|
||||
|
||||
for _, team := range query.Result.Teams {
|
||||
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/teams/: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 {
|
||||
return ApiError(404, "Team not found", err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to get Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &query.Result)
|
||||
}
|
||||
61
pkg/api/team_members.go
Normal file
61
pkg/api/team_members.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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/util"
|
||||
)
|
||||
|
||||
// GET /api/teams/:teamId/members
|
||||
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)
|
||||
}
|
||||
|
||||
for _, member := range query.Result {
|
||||
member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
// POST /api/teams/:teamId/members
|
||||
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.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)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{
|
||||
"message": "Member added to Team",
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/teams/:teamId/members/:userId
|
||||
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")
|
||||
}
|
||||
71
pkg/api/team_test.go
Normal file
71
pkg/api/team_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestTeamApiEndpoint(t *testing.T) {
|
||||
Convey("Given two teams", t, func() {
|
||||
mockResult := models.SearchTeamQueryResult{
|
||||
Teams: []*models.SearchTeamDto{
|
||||
{Name: "team1"},
|
||||
{Name: "team2"},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}
|
||||
|
||||
Convey("When searching with no parameters", func() {
|
||||
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchTeams
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 1000)
|
||||
So(sendPage, ShouldEqual, 1)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
|
||||
So(len(respJSON.Get("teams").MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When searching with page and perpage parameters", func() {
|
||||
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchTeams
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -32,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 {
|
||||
@@ -54,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)
|
||||
@@ -68,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")
|
||||
|
||||
@@ -107,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"))
|
||||
}
|
||||
|
||||
@@ -145,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) {
|
||||
@@ -162,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) {
|
||||
@@ -178,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)
|
||||
}
|
||||
@@ -210,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)
|
||||
@@ -219,8 +219,8 @@ func SearchUsers(c *middleware.Context) Response {
|
||||
return Json(200, query.Result.Users)
|
||||
}
|
||||
|
||||
// GET /api/search
|
||||
func SearchUsersWithPaging(c *middleware.Context) Response {
|
||||
// GET /api/users/search
|
||||
func SearchUsersWithPaging(c *m.ReqContext) Response {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
@@ -229,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
|
||||
@@ -247,13 +247,17 @@ func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range query.Result.Users {
|
||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func SetHelpFlag(c *middleware.Context) Response {
|
||||
func SetHelpFlag(c *m.ReqContext) Response {
|
||||
flag := c.ParamsInt64(":id")
|
||||
|
||||
bitmask := &c.HelpFlags1
|
||||
@@ -271,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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
@@ -31,7 +30,7 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
|
||||
)
|
||||
|
||||
var version = "4.6.0"
|
||||
var version = "5.0.0"
|
||||
var commit = "NA"
|
||||
var buildstamp string
|
||||
var build_date string
|
||||
@@ -41,9 +40,6 @@ var homePath = flag.String("homepath", "", "path to grafana install/home path, d
|
||||
var pidFile = flag.String("pidfile", "", "path to pid file")
|
||||
var exitChan = make(chan int)
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
v := flag.Bool("v", false, "prints current version and exits")
|
||||
profile := flag.Bool("profile", false, "Turn on pprof profiling")
|
||||
@@ -83,17 +79,28 @@ func main() {
|
||||
setting.BuildStamp = buildstampInt64
|
||||
|
||||
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
|
||||
|
||||
shutdownCompleted := make(chan int)
|
||||
server := NewGrafanaServer()
|
||||
server.Start()
|
||||
|
||||
go listenToSystemSignals(server, shutdownCompleted)
|
||||
|
||||
go func() {
|
||||
code := 0
|
||||
if err := server.Start(); err != nil {
|
||||
log.Error2("Startup failed", "error", err)
|
||||
code = 1
|
||||
}
|
||||
|
||||
exitChan <- code
|
||||
}()
|
||||
|
||||
code := <-shutdownCompleted
|
||||
log.Info2("Grafana shutdown completed.", "code", code)
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func initSql() {
|
||||
sqlstore.NewEngine()
|
||||
sqlstore.EnsureAdminUser()
|
||||
}
|
||||
|
||||
func listenToSystemSignals(server models.GrafanaServer) {
|
||||
func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
ignoreChan := make(chan os.Signal, 1)
|
||||
code := 0
|
||||
@@ -103,10 +110,12 @@ func listenToSystemSignals(server models.GrafanaServer) {
|
||||
|
||||
select {
|
||||
case sig := <-signalChan:
|
||||
// Stops trace if profiling has been enabled
|
||||
trace.Stop()
|
||||
trace.Stop() // Stops trace if profiling has been enabled
|
||||
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
|
||||
shutdownCompleted <- 0
|
||||
case code = <-exitChan:
|
||||
trace.Stop() // Stops trace if profiling has been enabled
|
||||
server.Shutdown(code, "startup error")
|
||||
shutdownCompleted <- code
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,35 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||
"github.com/grafana/grafana/pkg/services/eventpublisher"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
"github.com/grafana/grafana/pkg/tracing"
|
||||
)
|
||||
|
||||
func NewGrafanaServer() models.GrafanaServer {
|
||||
func NewGrafanaServer() *GrafanaServerImpl {
|
||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||
childRoutines, childCtx := errgroup.WithContext(rootCtx)
|
||||
|
||||
@@ -48,27 +52,32 @@ type GrafanaServerImpl struct {
|
||||
httpServer *api.HttpServer
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() {
|
||||
go listenToSystemSignals(g)
|
||||
|
||||
func (g *GrafanaServerImpl) Start() error {
|
||||
g.initLogging()
|
||||
g.writePIDFile()
|
||||
|
||||
initSql()
|
||||
|
||||
metrics.Init(setting.Cfg)
|
||||
search.Init()
|
||||
login.Init()
|
||||
social.NewOAuthService()
|
||||
eventpublisher.Init()
|
||||
plugins.Init()
|
||||
|
||||
closer, err := tracing.Init(setting.Cfg)
|
||||
pluginManager, err := plugins.NewPluginManager(g.context)
|
||||
if err != nil {
|
||||
g.log.Error("Tracing settings is not valid", "error", err)
|
||||
g.Shutdown(1, "Startup failed")
|
||||
return
|
||||
return fmt.Errorf("Failed to start plugins. error: %v", err)
|
||||
}
|
||||
defer closer.Close()
|
||||
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)
|
||||
}
|
||||
|
||||
tracingCloser, err := tracing.Init(setting.Cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Tracing settings is not valid. error: %v", err)
|
||||
}
|
||||
defer tracingCloser.Close()
|
||||
|
||||
// init alerting
|
||||
if setting.AlertingEnabled && setting.ExecuteAlerts {
|
||||
@@ -81,12 +90,17 @@ func (g *GrafanaServerImpl) Start() {
|
||||
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
|
||||
|
||||
if err = notifications.Init(); err != nil {
|
||||
g.log.Error("Notification service failed to initialize", "error", err)
|
||||
g.Shutdown(1, "Startup failed")
|
||||
return
|
||||
return fmt.Errorf("Notification service failed to initialize. error: %v", err)
|
||||
}
|
||||
|
||||
g.startHttpServer()
|
||||
sendSystemdNotification("READY=1")
|
||||
|
||||
return g.startHttpServer()
|
||||
}
|
||||
|
||||
func initSql() {
|
||||
sqlstore.NewEngine()
|
||||
sqlstore.EnsureAdminUser()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) initLogging() {
|
||||
@@ -105,16 +119,16 @@ func (g *GrafanaServerImpl) initLogging() {
|
||||
setting.LogConfigurationInfo()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() {
|
||||
func (g *GrafanaServerImpl) startHttpServer() error {
|
||||
g.httpServer = api.NewHttpServer()
|
||||
|
||||
err := g.httpServer.Start(g.context)
|
||||
|
||||
if err != nil {
|
||||
g.log.Error("Fail to start server", "error", err)
|
||||
g.Shutdown(1, "Startup failed")
|
||||
return
|
||||
return fmt.Errorf("Fail to start server. error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
@@ -127,10 +141,9 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
|
||||
g.shutdownFn()
|
||||
err = g.childRoutines.Wait()
|
||||
|
||||
g.log.Info("Shutdown completed", "reason", err)
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
if err != nil && err != context.Canceled {
|
||||
g.log.Error("Server shutdown completed with an error", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) writePIDFile() {
|
||||
@@ -154,3 +167,28 @@ func (g *GrafanaServerImpl) writePIDFile() {
|
||||
|
||||
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
|
||||
}
|
||||
|
||||
func sendSystemdNotification(state string) error {
|
||||
notifySocket := os.Getenv("NOTIFY_SOCKET")
|
||||
|
||||
if notifySocket == "" {
|
||||
return fmt.Errorf("NOTIFY_SOCKET environment variable empty or unset.")
|
||||
}
|
||||
|
||||
socketAddr := &net.UnixAddr{
|
||||
Name: notifySocket,
|
||||
Net: "unixgram",
|
||||
}
|
||||
|
||||
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Write([]byte(state))
|
||||
|
||||
conn.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
320
pkg/components/imguploader/azureblobuploader.go
Normal file
320
pkg/components/imguploader/azureblobuploader.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package imguploader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type AzureBlobUploader struct {
|
||||
account_name string
|
||||
account_key string
|
||||
container_name string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewAzureBlobUploader(account_name string, account_key string, container_name string) *AzureBlobUploader {
|
||||
return &AzureBlobUploader{
|
||||
account_name: account_name,
|
||||
account_key: account_key,
|
||||
container_name: container_name,
|
||||
log: log.New("azureBlobUploader"),
|
||||
}
|
||||
}
|
||||
|
||||
// Receive path of image on disk and return azure blob url
|
||||
func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
|
||||
// setup client
|
||||
blob := NewStorageClient(az.account_name, az.account_key)
|
||||
|
||||
file, err := os.Open(imageDiskPath)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
randomFileName := util.GetRandomString(30) + ".png"
|
||||
// upload image
|
||||
az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName)
|
||||
resp, err := blob.FileUpload(az.container_name, randomFileName, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode > 400 && resp.StatusCode < 600 {
|
||||
body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
aerr := &Error{
|
||||
Code: resp.StatusCode,
|
||||
Status: resp.Status,
|
||||
Body: body,
|
||||
Header: resp.Header,
|
||||
}
|
||||
aerr.parseXML()
|
||||
resp.Body.Close()
|
||||
return "", aerr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", az.account_name, az.container_name, randomFileName)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// --- AZURE LIBRARY
|
||||
type Blobs struct {
|
||||
XMLName xml.Name `xml:"EnumerationResults"`
|
||||
Items []Blob `xml:"Blobs>Blob"`
|
||||
}
|
||||
|
||||
type Blob struct {
|
||||
Name string `xml:"Name"`
|
||||
Property Property `xml:"Properties"`
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
LastModified string `xml:"Last-Modified"`
|
||||
Etag string `xml:"Etag"`
|
||||
ContentLength int `xml:"Content-Length"`
|
||||
ContentType string `xml:"Content-Type"`
|
||||
BlobType string `xml:"BlobType"`
|
||||
LeaseStatus string `xml:"LeaseStatus"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code int
|
||||
Status string
|
||||
Body []byte
|
||||
Header http.Header
|
||||
|
||||
AzureCode string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("status %d: %s", e.Code, e.Body)
|
||||
}
|
||||
|
||||
func (e *Error) parseXML() {
|
||||
var xe xmlError
|
||||
_ = xml.NewDecoder(bytes.NewReader(e.Body)).Decode(&xe)
|
||||
e.AzureCode = xe.Code
|
||||
}
|
||||
|
||||
type xmlError struct {
|
||||
XMLName xml.Name `xml:"Error"`
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
const ms_date_layout = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||
const version = "2017-04-17"
|
||||
|
||||
var client = &http.Client{}
|
||||
|
||||
type StorageClient struct {
|
||||
Auth *Auth
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (c *StorageClient) transport() http.RoundTripper {
|
||||
if c.Transport != nil {
|
||||
return c.Transport
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
func NewStorageClient(account, accessKey string) *StorageClient {
|
||||
return &StorageClient{
|
||||
Auth: &Auth{
|
||||
account,
|
||||
accessKey,
|
||||
},
|
||||
Transport: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StorageClient) absUrl(format string, a ...interface{}) string {
|
||||
part := fmt.Sprintf(format, a...)
|
||||
return fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.Auth.Account, part)
|
||||
}
|
||||
|
||||
func copyHeadersToRequest(req *http.Request, headers map[string]string) {
|
||||
for k, v := range headers {
|
||||
req.Header[k] = []string{v}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StorageClient) FileUpload(container, blobName string, body io.Reader) (*http.Response, error) {
|
||||
blobName = escape(blobName)
|
||||
extension := strings.ToLower(path.Ext(blobName))
|
||||
contentType := mime.TypeByExtension(extension)
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(body)
|
||||
req, err := http.NewRequest(
|
||||
"PUT",
|
||||
c.absUrl("%s/%s", container, blobName),
|
||||
buf,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
copyHeadersToRequest(req, map[string]string{
|
||||
"x-ms-blob-type": "BlockBlob",
|
||||
"x-ms-date": time.Now().UTC().Format(ms_date_layout),
|
||||
"x-ms-version": version,
|
||||
"Accept-Charset": "UTF-8",
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": strconv.Itoa(buf.Len()),
|
||||
})
|
||||
|
||||
c.Auth.SignRequest(req)
|
||||
|
||||
return c.transport().RoundTrip(req)
|
||||
}
|
||||
|
||||
func escape(content string) string {
|
||||
content = url.QueryEscape(content)
|
||||
// the Azure's behavior uses %20 to represent whitespace instead of + (plus)
|
||||
content = strings.Replace(content, "+", "%20", -1)
|
||||
// the Azure's behavior uses slash instead of + %2F
|
||||
content = strings.Replace(content, "%2F", "/", -1)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Account string
|
||||
Key string
|
||||
}
|
||||
|
||||
func (a *Auth) SignRequest(req *http.Request) {
|
||||
strToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s",
|
||||
strings.ToUpper(req.Method),
|
||||
tryget(req.Header, "Content-Encoding"),
|
||||
tryget(req.Header, "Content-Language"),
|
||||
tryget(req.Header, "Content-Length"),
|
||||
tryget(req.Header, "Content-MD5"),
|
||||
tryget(req.Header, "Content-Type"),
|
||||
tryget(req.Header, "Date"),
|
||||
tryget(req.Header, "If-Modified-Since"),
|
||||
tryget(req.Header, "If-Match"),
|
||||
tryget(req.Header, "If-None-Match"),
|
||||
tryget(req.Header, "If-Unmodified-Since"),
|
||||
tryget(req.Header, "Range"),
|
||||
a.canonicalizedHeaders(req),
|
||||
a.canonicalizedResource(req),
|
||||
)
|
||||
decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
|
||||
|
||||
sha256 := hmac.New(sha256.New, []byte(decodedKey))
|
||||
sha256.Write([]byte(strToSign))
|
||||
|
||||
signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
|
||||
|
||||
copyHeadersToRequest(req, map[string]string{
|
||||
"Authorization": fmt.Sprintf("SharedKey %s:%s", a.Account, signature),
|
||||
})
|
||||
}
|
||||
|
||||
func tryget(headers map[string][]string, key string) string {
|
||||
// We default to empty string for "0" values to match server side behavior when generating signatures.
|
||||
if len(headers[key]) > 0 { // && headers[key][0] != "0" { //&& key != "Content-Length" {
|
||||
return headers[key][0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//
|
||||
// The following is copied ~95% verbatim from:
|
||||
// http://github.com/loldesign/azure/ -> core/core.go
|
||||
//
|
||||
|
||||
/*
|
||||
Based on Azure docs:
|
||||
Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
|
||||
|
||||
1) Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header.
|
||||
2) Convert each HTTP header name to lowercase.
|
||||
3) Sort the headers lexicographically by header name, in ascending order. Note that each header may appear only once in the string.
|
||||
4) Unfold the string by replacing any breaking white space with a single space.
|
||||
5) Trim any white space around the colon in the header.
|
||||
6) Finally, append a new line character to each canonicalized header in the resulting list. Construct the CanonicalizedHeaders string by concatenating all headers in this list into a single string.
|
||||
*/
|
||||
func (a *Auth) canonicalizedHeaders(req *http.Request) string {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
for key, value := range req.Header {
|
||||
lowerKey := strings.ToLower(key)
|
||||
|
||||
if strings.HasPrefix(lowerKey, "x-ms-") {
|
||||
if buffer.Len() == 0 {
|
||||
buffer.WriteString(fmt.Sprintf("%s:%s", lowerKey, value[0]))
|
||||
} else {
|
||||
buffer.WriteString(fmt.Sprintf("\n%s:%s", lowerKey, value[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splitted := strings.Split(buffer.String(), "\n")
|
||||
sort.Strings(splitted)
|
||||
|
||||
return strings.Join(splitted, "\n")
|
||||
}
|
||||
|
||||
/*
|
||||
Based on Azure docs
|
||||
Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
|
||||
|
||||
1) Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns the resource being accessed.
|
||||
2) Append the resource's encoded URI path, without any query parameters.
|
||||
3) Retrieve all query parameters on the resource URI, including the comp parameter if it exists.
|
||||
4) Convert all parameter names to lowercase.
|
||||
5) Sort the query parameters lexicographically by parameter name, in ascending order.
|
||||
6) URL-decode each query parameter name and value.
|
||||
7) Append each query parameter name and value to the string in the following format, making sure to include the colon (:) between the name and the value:
|
||||
parameter-name:parameter-value
|
||||
|
||||
8) If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list:
|
||||
parameter-name:parameter-value-1,parameter-value-2,parameter-value-n
|
||||
|
||||
9) Append a new line character (\n) after each name-value pair.
|
||||
|
||||
Rules:
|
||||
1) Avoid using the new line character (\n) in values for query parameters. If it must be used, ensure that it does not affect the format of the canonicalized resource string.
|
||||
2) Avoid using commas in query parameter values.
|
||||
*/
|
||||
func (a *Auth) canonicalizedResource(req *http.Request) string {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
buffer.WriteString(fmt.Sprintf("/%s%s", a.Account, req.URL.Path))
|
||||
queries := req.URL.Query()
|
||||
|
||||
for key, values := range queries {
|
||||
sort.Strings(values)
|
||||
buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ",")))
|
||||
}
|
||||
|
||||
splitted := strings.Split(buffer.String(), "\n")
|
||||
sort.Strings(splitted)
|
||||
|
||||
return strings.Join(splitted, "\n")
|
||||
}
|
||||
24
pkg/components/imguploader/azureblobuploader_test.go
Normal file
24
pkg/components/imguploader/azureblobuploader_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package imguploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUploadToAzureBlob(t *testing.T) {
|
||||
SkipConvey("[Integration test] for external_image_store.azure_blob", t, func() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
uploader, _ := NewImageUploader()
|
||||
|
||||
path, err := uploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(path, ShouldNotEqual, "")
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -20,19 +21,22 @@ const (
|
||||
type GCSUploader struct {
|
||||
keyFile string
|
||||
bucket string
|
||||
path string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewGCSUploader(keyFile, bucket string) *GCSUploader {
|
||||
func NewGCSUploader(keyFile, bucket, path string) *GCSUploader {
|
||||
return &GCSUploader{
|
||||
keyFile: keyFile,
|
||||
bucket: bucket,
|
||||
path: path,
|
||||
log: log.New("gcsuploader"),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *GCSUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
|
||||
key := util.GetRandomString(20) + ".png"
|
||||
fileName := util.GetRandomString(20) + ".png"
|
||||
key := path.Join(u.path, fileName)
|
||||
|
||||
u.log.Debug("Opening key file ", u.keyFile)
|
||||
data, err := ioutil.ReadFile(u.keyFile)
|
||||
|
||||
@@ -3,6 +3,7 @@ package imguploader
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -73,8 +74,26 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
keyFile := gcssec.Key("key_file").MustString("")
|
||||
bucketName := gcssec.Key("bucket").MustString("")
|
||||
path := gcssec.Key("path").MustString("")
|
||||
|
||||
return NewGCSUploader(keyFile, bucketName), nil
|
||||
return NewGCSUploader(keyFile, bucketName, path), nil
|
||||
case "azure_blob":
|
||||
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account_name := azureBlobSec.Key("account_name").MustString("")
|
||||
account_key := azureBlobSec.Key("account_key").MustString("")
|
||||
container_name := azureBlobSec.Key("container_name").MustString("")
|
||||
|
||||
return NewAzureBlobUploader(account_name, account_key, container_name), nil
|
||||
case "local":
|
||||
return NewLocalImageUploader()
|
||||
}
|
||||
|
||||
if setting.ImageUploadProvider != "" {
|
||||
log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
||||
}
|
||||
|
||||
return NopImageUploader{}, nil
|
||||
|
||||
@@ -119,5 +119,47 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json")
|
||||
So(original.bucket, ShouldEqual, "project-grafana-east")
|
||||
})
|
||||
|
||||
Convey("AzureBlobUploader config", func() {
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
setting.ImageUploadProvider = "azure_blob"
|
||||
|
||||
Convey("with container name", func() {
|
||||
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
|
||||
azureBlobSec.NewKey("account_name", "account_name")
|
||||
azureBlobSec.NewKey("account_key", "account_key")
|
||||
azureBlobSec.NewKey("container_name", "container_name")
|
||||
|
||||
uploader, err := NewImageUploader()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*AzureBlobUploader)
|
||||
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.account_name, ShouldEqual, "account_name")
|
||||
So(original.account_key, ShouldEqual, "account_key")
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
22
pkg/components/imguploader/localuploader.go
Normal file
22
pkg/components/imguploader/localuploader.go
Normal 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
|
||||
}
|
||||
18
pkg/components/imguploader/localuploader_test.go
Normal file
18
pkg/components/imguploader/localuploader_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
@@ -16,17 +16,22 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type RenderOpts struct {
|
||||
Path string
|
||||
Width string
|
||||
Height string
|
||||
Timeout string
|
||||
OrgId int64
|
||||
Timezone string
|
||||
Path string
|
||||
Width string
|
||||
Height string
|
||||
Timeout string
|
||||
OrgId int64
|
||||
UserId int64
|
||||
OrgRole models.RoleType
|
||||
Timezone string
|
||||
IsAlertContext bool
|
||||
Encoding string
|
||||
}
|
||||
|
||||
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
|
||||
@@ -67,14 +72,20 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
||||
localDomain = setting.HttpAddr
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s://%s:%s/%s", setting.Protocol, localDomain, setting.HttpPort, params.Path)
|
||||
// &render=1 signals to the legacy redirect layer to
|
||||
// avoid redirect these requests.
|
||||
url := fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, localDomain, setting.HttpPort, params.Path)
|
||||
|
||||
binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
|
||||
scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
|
||||
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
|
||||
pngPath = pngPath + ".png"
|
||||
|
||||
renderKey := middleware.AddRenderAuthKey(params.OrgId)
|
||||
orgRole := params.OrgRole
|
||||
if params.IsAlertContext {
|
||||
orgRole = models.ROLE_ADMIN
|
||||
}
|
||||
renderKey := middleware.AddRenderAuthKey(params.OrgId, params.UserId, orgRole)
|
||||
defer middleware.RemoveRenderAuthKey(renderKey)
|
||||
|
||||
timeout, err := strconv.Atoi(params.Timeout)
|
||||
@@ -82,9 +93,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,
|
||||
@@ -95,16 +112,18 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
||||
"renderKey=" + renderKey,
|
||||
}
|
||||
|
||||
if params.Encoding != "" {
|
||||
cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", params.Encoding)}, cmdArgs...)
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -113,15 +132,18 @@ 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() {
|
||||
cmd.Wait()
|
||||
if err := cmd.Wait(); err != nil {
|
||||
rendererLog.Error("failed to render an image", "error", err)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -124,6 +125,30 @@ func (w *FileLogWriter) createLogFile() (*os.File, error) {
|
||||
return os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||
}
|
||||
|
||||
func (w *FileLogWriter) lineCounter() (int, error) {
|
||||
r, err := os.OpenFile(w.Filename, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("lineCounter Open File : %s", err)
|
||||
}
|
||||
buf := make([]byte, 32*1024)
|
||||
count := 0
|
||||
|
||||
for {
|
||||
c, err := r.Read(buf)
|
||||
count += bytes.Count(buf[:c], []byte{'\n'})
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
if err := r.Close(); err != nil {
|
||||
return count, err
|
||||
}
|
||||
return count, nil
|
||||
|
||||
case err != nil:
|
||||
return count, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *FileLogWriter) initFd() error {
|
||||
fd := w.mw.fd
|
||||
finfo, err := fd.Stat()
|
||||
@@ -133,11 +158,11 @@ func (w *FileLogWriter) initFd() error {
|
||||
w.maxsize_cursize = int(finfo.Size())
|
||||
w.daily_opendate = time.Now().Day()
|
||||
if finfo.Size() > 0 {
|
||||
content, err := ioutil.ReadFile(w.Filename)
|
||||
count, err := w.lineCounter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.maxlines_curlines = len(strings.Split(string(content), "\n"))
|
||||
w.maxlines_curlines = count
|
||||
} else {
|
||||
w.maxlines_curlines = 0
|
||||
}
|
||||
|
||||
45
pkg/log/file_test.go
Normal file
45
pkg/log/file_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func (w *FileLogWriter) WriteLine(line string) error {
|
||||
n, err := w.mw.Write([]byte(line))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.docheck(n)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestLogFile(t *testing.T) {
|
||||
|
||||
Convey("When logging to file", t, func() {
|
||||
fileLogWrite := NewFileWriter()
|
||||
So(fileLogWrite, ShouldNotBeNil)
|
||||
|
||||
fileLogWrite.Filename = "grafana_test.log"
|
||||
err := fileLogWrite.Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Log file is empty", func() {
|
||||
So(fileLogWrite.maxlines_curlines, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Logging should add lines", func() {
|
||||
err := fileLogWrite.WriteLine("test1\n")
|
||||
err = fileLogWrite.WriteLine("test2\n")
|
||||
err = fileLogWrite.WriteLine("test3\n")
|
||||
So(err, ShouldBeNil)
|
||||
So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)
|
||||
})
|
||||
|
||||
fileLogWrite.Close()
|
||||
err = os.Remove(fileLogWrite.Filename)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
@@ -14,13 +14,14 @@ import (
|
||||
|
||||
"github.com/go-stack/stack"
|
||||
"github.com/inconshreveable/log15"
|
||||
"github.com/inconshreveable/log15/term"
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
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,
|
||||
@@ -157,7 +177,7 @@ func getFilters(filterStrArray []string) map[string]log15.Lvl {
|
||||
func getLogFormat(format string) log15.Format {
|
||||
switch format {
|
||||
case "console":
|
||||
if term.IsTty(os.Stdout.Fd()) {
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
return log15.TerminalFormat()
|
||||
}
|
||||
return log15.LogfmtFormat()
|
||||
@@ -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
39
pkg/log/log_writer.go
Normal 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
116
pkg/log/log_writer_test.go
Normal 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")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
214
pkg/login/auth_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
48
pkg/login/brute_force_login_protection.go
Normal file
48
pkg/login/brute_force_login_protection.go
Normal 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)
|
||||
}
|
||||
125
pkg/login/brute_force_login_protection_test.go
Normal file
125
pkg/login/brute_force_login_protection_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
35
pkg/login/grafana_login.go
Normal file
35
pkg/login/grafana_login.go
Normal 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
|
||||
}
|
||||
139
pkg/login/grafana_login_test.go
Normal file
139
pkg/login/grafana_login_test.go
Normal 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
21
pkg/login/ldap_login.go
Normal 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
|
||||
}
|
||||
172
pkg/login/ldap_login_test.go
Normal file
172
pkg/login/ldap_login_test.go
Normal 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)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ func init() {
|
||||
|
||||
M_DataSource_ProxyReq_Timer = prometheus.NewSummary(prometheus.SummaryOpts{
|
||||
Name: "api_dataproxy_request_all_milliseconds",
|
||||
Help: "summary for dashboard search duration",
|
||||
Help: "summary for dataproxy request duration",
|
||||
Namespace: exporterName,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ":")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
55
pkg/middleware/dashboard_redirect.go
Normal file
55
pkg/middleware/dashboard_redirect.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"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")
|
||||
renderRequest := c.QueryBool("render")
|
||||
|
||||
if slug != "" {
|
||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||
if renderRequest && strings.Contains(url, setting.AppSubUrl) {
|
||||
url = strings.Replace(url, setting.AppSubUrl, "", 1)
|
||||
}
|
||||
|
||||
url = strings.Replace(url, "/d/", "/d-solo/", 1)
|
||||
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
|
||||
c.Redirect(url, 301)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
pkg/middleware/dashboard_redirect_test.go
Normal file
58
pkg/middleware/dashboard_redirect_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -87,16 +74,16 @@ func initContextWithAnonymousUser(ctx *Context) bool {
|
||||
|
||||
ctx.IsSignedIn = false
|
||||
ctx.AllowAnonymous = true
|
||||
ctx.SignedInUser = &m.SignedInUser{}
|
||||
ctx.SignedInUser = &m.SignedInUser{IsAnonymous: true}
|
||||
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
|
||||
ctx.OrgId = orgQuery.Result.Id
|
||||
ctx.OrgName = orgQuery.Result.Name
|
||||
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")
|
||||
|
||||
@@ -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,13 +357,14 @@ func middlewareScenario(desc string, fn scenarioFunc) {
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *Context
|
||||
context *m.ReqContext
|
||||
resp *httptest.ResponseRecorder
|
||||
apiKey string
|
||||
authHeader string
|
||||
respJson map[string]interface{}
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
url string
|
||||
|
||||
req *http.Request
|
||||
}
|
||||
@@ -398,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
|
||||
@@ -421,4 +437,4 @@ func (sc *scenarioContext) exec() {
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
type handlerFunc func(c *Context)
|
||||
type handlerFunc func(c *m.ReqContext)
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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"
|
||||
@@ -21,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
|
||||
}
|
||||
@@ -30,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)
|
||||
@@ -41,7 +42,7 @@ func OrgRedirect() macaron.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
newUrl := setting.ToAbsUrl(fmt.Sprintf("%s?%s", c.Req.URL.Path, c.Req.URL.Query().Encode()))
|
||||
c.Redirect(newUrl, 302)
|
||||
newURL := setting.ToAbsUrl(fmt.Sprintf("%s?%s", strings.TrimPrefix(c.Req.URL.Path, "/"), c.Req.URL.Query().Encode()))
|
||||
c.Redirect(newURL, 302)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user