Merge branch 'master' into feature/discord

This commit is contained in:
Andrzej Ressel
2018-04-08 20:09:56 +02:00
4486 changed files with 636351 additions and 459241 deletions
+11 -3
View File
@@ -1,15 +1,15 @@
package api
import (
"regexp"
"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() {
@@ -22,6 +22,14 @@ func AdminGetSettings(c *middleware.Context) {
if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config")) {
value = "************"
}
if strings.Contains(keyName, "url") {
var rgx = regexp.MustCompile(`.*:\/\/([^:]*):([^@]*)@.*?$`)
var subs = rgx.FindAllSubmatch([]byte(value), -1)
if subs != nil && len(subs[0]) == 3 {
value = strings.Replace(value, string(subs[0][1]), "******", 1)
value = strings.Replace(value, string(subs[0][2]), "******", 1)
}
}
jsonSec[keyName] = value
}
@@ -30,7 +38,7 @@ func AdminGetSettings(c *middleware.Context) {
c.JSON(200, settings)
}
func AdminGetStats(c *middleware.Context) {
func AdminGetStats(c *m.ReqContext) {
statsQuery := m.GetAdminStatsQuery{}
+12 -13
View File
@@ -4,12 +4,11 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) {
cmd := m.CreateUserCommand{
Login: form.Login,
Email: form.Email,
@@ -35,7 +34,7 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
return
}
metrics.M_Api_Admin_User_Create.Inc(1)
metrics.M_Api_Admin_User_Create.Inc()
user := cmd.Result
@@ -47,15 +46,15 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
c.JSON(200, result)
}
func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) {
userId := c.ParamsInt64(":id")
func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) {
userID := c.ParamsInt64(":id")
if len(form.Password) < 4 {
c.JsonApiErr(400, "New password too short", nil)
return
}
userQuery := m.GetUserByIdQuery{Id: userId}
userQuery := m.GetUserByIdQuery{Id: userID}
if err := bus.Dispatch(&userQuery); err != nil {
c.JsonApiErr(500, "Could not read user from database", err)
@@ -65,7 +64,7 @@ func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPas
passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt)
cmd := m.ChangeUserPasswordCommand{
UserId: userId,
UserId: userID,
NewPassword: passwordHashed,
}
@@ -77,11 +76,11 @@ func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPas
c.JsonOK("User password updated")
}
func AdminUpdateUserPermissions(c *middleware.Context, form dtos.AdminUpdateUserPermissionsForm) {
userId := c.ParamsInt64(":id")
func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
userID := c.ParamsInt64(":id")
cmd := m.UpdateUserPermissionsCommand{
UserId: userId,
UserId: userID,
IsGrafanaAdmin: form.IsGrafanaAdmin,
}
@@ -93,10 +92,10 @@ func AdminUpdateUserPermissions(c *middleware.Context, form dtos.AdminUpdateUser
c.JsonOK("User permissions updated")
}
func AdminDeleteUser(c *middleware.Context) {
userId := c.ParamsInt64(":id")
func AdminDeleteUser(c *m.ReqContext) {
userID := c.ParamsInt64(":id")
cmd := m.DeleteUserCommand{UserId: userId}
cmd := m.DeleteUserCommand{UserId: userID}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to delete user", err)
+85 -115
View File
@@ -5,14 +5,14 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/guardian"
)
func ValidateOrgAlert(c *middleware.Context) {
func ValidateOrgAlert(c *m.ReqContext) {
id := c.ParamsInt64(":alertId")
query := models.GetAlertByIdQuery{Id: id}
query := m.GetAlertByIdQuery{Id: id}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(404, "Alert not found", nil)
@@ -25,32 +25,33 @@ func ValidateOrgAlert(c *middleware.Context) {
}
}
func GetAlertStatesForDashboard(c *middleware.Context) Response {
dashboardId := c.QueryInt64("dashboardId")
func GetAlertStatesForDashboard(c *m.ReqContext) Response {
dashboardID := c.QueryInt64("dashboardId")
if dashboardId == 0 {
return ApiError(400, "Missing query parameter dashboardId", nil)
if dashboardID == 0 {
return Error(400, "Missing query parameter dashboardId", nil)
}
query := models.GetAlertStatesForDashboardQuery{
query := m.GetAlertStatesForDashboardQuery{
OrgId: c.OrgId,
DashboardId: c.QueryInt64("dashboardId"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch alert states", err)
return Error(500, "Failed to fetch alert states", err)
}
return Json(200, query.Result)
return JSON(200, query.Result)
}
// 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")
@@ -59,52 +60,20 @@ func GetAlerts(c *middleware.Context) Response {
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "List alerts failed", err)
return Error(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)
return Error(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil)
}
backendCmd := alerting.AlertTestCommand{
@@ -115,9 +84,9 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
if err := bus.Dispatch(&backendCmd); err != nil {
if validationErr, ok := err.(alerting.ValidationError); ok {
return ApiError(422, validationErr.Error(), nil)
return Error(422, validationErr.Error(), nil)
}
return ApiError(500, "Failed to test rule", err)
return Error(500, "Failed to test rule", err)
}
res := backendCmd.Result
@@ -140,48 +109,30 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs())
return Json(200, dtoRes)
return JSON(200, dtoRes)
}
// 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)
return Error(500, "List alerts failed", err)
}
return Json(200, &query.Result)
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 *m.ReqContext) Response {
return JSON(200, alerting.GetNotifiers())
}
func GetAlertNotifiers(c *middleware.Context) 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)
return Error(500, "Failed to get alert notifications", err)
}
result := make([]*dtos.AlertNotification, 0)
@@ -197,57 +148,57 @@ func GetAlertNotifications(c *middleware.Context) Response {
})
}
return Json(200, result)
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"),
}
if err := bus.Dispatch(query); err != nil {
return ApiError(500, "Failed to get alert notifications", err)
return Error(500, "Failed to get alert notifications", err)
}
return Json(200, query.Result)
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 {
return ApiError(500, "Failed to create alert notification", err)
return Error(500, "Failed to create alert notification", err)
}
return Json(200, cmd.Result)
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 {
return ApiError(500, "Failed to update alert notification", err)
return Error(500, "Failed to update alert notification", err)
}
return Json(200, cmd.Result)
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"),
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete alert notification", err)
return Error(500, "Failed to delete alert notification", err)
}
return ApiSuccess("Notification deleted")
return Success("Notification deleted")
}
//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,55 +206,74 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R
}
if err := bus.Dispatch(cmd); err != nil {
return ApiError(500, "Failed to send alert notifications", err)
if err == m.ErrSmtpNotEnabled {
return Error(412, err.Error(), err)
}
return Error(500, "Failed to send alert notifications", err)
}
return ApiSuccess("Test notification sent")
return Success("Test notification sent")
}
//POST /api/alerts/:alertId/pause
func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
alertId := c.ParamsInt64("alertId")
cmd := models.PauseAlertCommand{
func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
alertID := c.ParamsInt64("alertId")
query := m.GetAlertByIdQuery{Id: alertID}
if err := bus.Dispatch(&query); err != nil {
return Error(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 Error(500, "Error while checking permissions for Alert", err)
}
return Error(403, "Access denied to this dashboard and alert", nil)
}
cmd := m.PauseAlertCommand{
OrgId: c.OrgId,
AlertIds: []int64{alertId},
AlertIds: []int64{alertID},
Paused: dto.Paused,
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "", err)
return Error(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,
"alertId": alertID,
"state": response,
"message": "alert " + pausedState,
"message": "Alert " + pausedState,
}
return Json(200, result)
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,
}
if err := bus.Dispatch(&updateCmd); err != nil {
return ApiError(500, "Failed to pause alerts", err)
return Error(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"
}
@@ -313,5 +283,5 @@ func PauseAllAlerts(c *middleware.Context, dto dtos.PauseAllAlertsCommand) Respo
"alertsAffected": updateCmd.ResultCount,
}
return Json(200, result)
return JSON(200, result)
}
+96
View File
@@ -0,0 +1,96 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertingApiEndpoint(t *testing.T) {
Convey("Given an alert in a dashboard with an acl", t, func() {
singleAlert := &m.Alert{Id: 1, DashboardId: 1, Name: "singlealert"}
bus.AddHandler("test", func(query *m.GetAlertByIdQuery) error {
query.Result = singleAlert
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
Convey("When user is editor and not in the ACL", func() {
Convey("Should not be able to pause the alert", func() {
cmd := dtos.PauseAlertCommand{
AlertId: 1,
Paused: true,
}
postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
CallPauseAlert(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is editor and dashboard has default ACL", func() {
aclMockResp = []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
Convey("Should be able to pause the alert", func() {
cmd := dtos.PauseAlertCommand{
AlertId: 1,
Paused: true,
}
postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
CallPauseAlert(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
}
func CallPauseAlert(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.PauseAlertCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func postAlertScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PauseAlertCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return PauseAlert(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
+277 -20
View File
@@ -1,51 +1,223 @@
package api
import (
"strings"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/components/simplejson"
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,
To: c.QueryInt64("to") / 1000,
Type: annotations.ItemType(c.Query("type")),
OrgId: c.OrgId,
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"),
NewState: c.QueryStrings("newState"),
Tags: c.QueryStrings("tags"),
Type: c.Query("type"),
}
repo := annotations.GetRepository()
items, err := repo.Find(query)
if err != nil {
return ApiError(500, "Failed to get annotations", err)
return Error(500, "Failed to get annotations", err)
}
result := make([]dtos.Annotation, 0)
for _, item := range items {
result = append(result, dtos.Annotation{
AlertId: item.AlertId,
Time: item.Epoch * 1000,
Data: item.Data,
NewState: item.NewState,
PrevState: item.PrevState,
Text: item.Text,
Metric: item.Metric,
Title: item.Title,
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
item.Time = item.Time * 1000
}
return JSON(200, items)
}
type CreateAnnotationError struct {
message string
}
func (e *CreateAnnotationError) Error() string {
return e.message
}
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 == "" {
err := &CreateAnnotationError{"text field should not be empty"}
return Error(500, "Failed to save annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
Epoch: cmd.Time / 1000,
Text: cmd.Text,
Data: cmd.Data,
Tags: cmd.Tags,
}
if item.Epoch == 0 {
item.Epoch = time.Now().Unix()
}
if err := repo.Save(&item); err != nil {
return Error(500, "Failed to save annotation", err)
}
startID := item.Id
// handle regions
if cmd.IsRegion {
item.RegionId = startID
if item.Data == nil {
item.Data = simplejson.New()
}
if err := repo.Update(&item); err != nil {
return Error(500, "Failed set regionId on annotation", err)
}
item.Id = 0
item.Epoch = cmd.TimeEnd / 1000
if err := repo.Save(&item); err != nil {
return Error(500, "Failed save annotation for region end time", err)
}
return JSON(200, util.DynMap{
"message": "Annotation added",
"id": startID,
"endId": item.Id,
})
}
return Json(200, result)
return JSON(200, util.DynMap{
"message": "Annotation added",
"id": startID,
})
}
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
func formatGraphiteAnnotation(what string, data string) string {
text := what
if data != "" {
text = text + "\n" + data
}
return text
}
func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd) Response {
repo := annotations.GetRepository()
if cmd.What == "" {
err := &CreateAnnotationError{"what field should not be empty"}
return Error(500, "Failed to save Graphite annotation", err)
}
if cmd.When == 0 {
cmd.When = time.Now().Unix()
}
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
var tagsArray []string
switch tags := cmd.Tags.(type) {
case string:
if tags != "" {
tagsArray = strings.Split(tags, " ")
} else {
tagsArray = []string{}
}
case []interface{}:
for _, t := range tags {
if tagStr, ok := t.(string); ok {
tagsArray = append(tagsArray, tagStr)
} else {
err := &CreateAnnotationError{"tag should be a string"}
return Error(500, "Failed to save Graphite annotation", err)
}
}
default:
err := &CreateAnnotationError{"unsupported tags format"}
return Error(500, "Failed to save Graphite annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Epoch: cmd.When,
Text: text,
Tags: tagsArray,
}
if err := repo.Save(&item); err != nil {
return Error(500, "Failed to save Graphite annotation", err)
}
return JSON(200, util.DynMap{
"message": "Graphite annotation added",
"id": item.Id,
})
}
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,
Id: annotationID,
Epoch: cmd.Time / 1000,
Text: cmd.Text,
Tags: cmd.Tags,
}
if err := repo.Update(&item); err != nil {
return Error(500, "Failed to update annotation", err)
}
if cmd.IsRegion {
itemRight := item
itemRight.RegionId = item.Id
itemRight.Epoch = cmd.TimeEnd / 1000
// We don't know id of region right event, so set it to 0 and find then using query like
// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...
itemRight.Id = 0
if err := repo.Update(&itemRight); err != nil {
return Error(500, "Failed to update annotation for region end time", err)
}
}
return Success("Annotation updated")
}
func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
repo := annotations.GetRepository()
err := repo.Delete(&annotations.DeleteParams{
@@ -55,8 +227,93 @@ func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Res
})
if err != nil {
return ApiError(500, "Failed to delete annotations", err)
return Error(500, "Failed to delete annotations", err)
}
return ApiSuccess("Annotations deleted")
return Success("Annotations deleted")
}
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,
})
if err != nil {
return Error(500, "Failed to delete annotation", err)
}
return Success("Annotation deleted")
}
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,
})
if err != nil {
return Error(500, "Failed to delete annotation region", err)
}
return Success("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 Error(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 Error(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
View File
@@ -0,0 +1,241 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
. "github.com/smartystreets/goconvey/convey"
)
func TestAnnotationsApiEndpoint(t *testing.T) {
Convey("Given an annotation without a dashboard id", t, func() {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
}
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
Convey("Should not be allowed to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
Convey("Should be able to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
Convey("Given an annotation with a dashboard id and the dashboard does not have an acl", t, func() {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
DashboardId: 1,
PanelId: 1,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
IsRegion: false,
Id: 1,
}
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
return nil
})
Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER
Convey("Should not be allowed to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is an Org Editor", func() {
role := m.ROLE_EDITOR
Convey("Should be able to save an annotation", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/region/1", "/api/annotations/region/:regionId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationRegion
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
}
type fakeAnnotationsRepo struct {
}
func (repo *fakeAnnotationsRepo) Delete(params *annotations.DeleteParams) error {
return nil
}
func (repo *fakeAnnotationsRepo) Save(item *annotations.Item) error {
item.Id = 1
return nil
}
func (repo *fakeAnnotationsRepo) Update(item *annotations.Item) error {
return nil
}
func (repo *fakeAnnotationsRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
annotations := []*annotations.ItemDTO{{Id: 1}}
return annotations, nil
}
var fakeAnnoRepo *fakeAnnotationsRepo
func postAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PostAnnotationsCmd, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return PostAnnotation(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func putAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return UpdateAnnotation(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
})
}
+208 -137
View File
@@ -9,17 +9,21 @@ import (
)
// Register adds http routes
func (hs *HttpServer) registerRoutes() {
r := hs.macaron
func (hs *HTTPServer) registerRoutes() {
macaronR := hs.macaron
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
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
// automatically set HEAD for every GET
r.SetAutoHead(true)
macaronR.SetAutoHead(true)
r := newRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)
// not logged in views
r.Get("/", reqSignedIn, Index)
@@ -38,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)
@@ -56,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)
@@ -92,191 +107,245 @@ 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)
r.Get("/api/login/ping", quota("session"), LoginAPIPing)
// authed api
r.Group("/api", func() {
r.Group("/api", func(apiRoute RouteRegister) {
// user (signed in)
r.Group("/user", func() {
r.Get("/", wrap(GetSignedInUser))
r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
r.Post("/using/:id", wrap(UserSetUsingOrg))
r.Get("/orgs", wrap(GetSignedInUserOrgList))
apiRoute.Group("/user", func(userRoute RouteRegister) {
userRoute.Get("/", wrap(GetSignedInUser))
userRoute.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
userRoute.Post("/using/:id", wrap(UserSetUsingOrg))
userRoute.Get("/orgs", wrap(GetSignedInUserOrgList))
r.Post("/stars/dashboard/:id", wrap(StarDashboard))
r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
userRoute.Post("/stars/dashboard/:id", wrap(StarDashboard))
userRoute.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
r.Get("/quotas", wrap(GetUserQuotas))
r.Put("/helpflags/:id", wrap(SetHelpFlag))
userRoute.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
userRoute.Get("/quotas", wrap(GetUserQuotas))
userRoute.Put("/helpflags/:id", wrap(SetHelpFlag))
// For dev purpose
r.Get("/helpflags/clear", wrap(ClearHelpFlags))
userRoute.Get("/helpflags/clear", wrap(ClearHelpFlags))
r.Get("/preferences", wrap(GetUserPreferences))
r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
userRoute.Get("/preferences", wrap(GetUserPreferences))
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
})
// users (admin permission required)
r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/search", wrap(SearchUsersWithPaging))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
apiRoute.Group("/users", func(usersRoute RouteRegister) {
usersRoute.Get("/", wrap(SearchUsers))
usersRoute.Get("/search", wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", wrap(GetUserByID))
usersRoute.Get("/:id/orgs", wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
r.Get("/lookup", wrap(GetUserByLoginOrEmail))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail))
usersRoute.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
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.
r.Group("/org", func() {
r.Get("/", wrap(GetOrgCurrent))
r.Get("/quotas", wrap(GetOrgQuotas))
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent))
orgRoute.Get("/quotas", wrap(GetOrgQuotas))
})
// current org
r.Group("/org", func() {
r.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
r.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
r.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
// invites
r.Get("/invites", wrap(GetPendingOrgInvites))
r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
orgRoute.Get("/invites", wrap(GetPendingOrgInvites))
orgRoute.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", wrap(RevokeInvite))
// prefs
r.Get("/preferences", wrap(GetOrgPreferences))
r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
orgRoute.Get("/preferences", wrap(GetOrgPreferences))
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
}, reqOrgAdmin)
// create new org
r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
// search all orgs
r.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
apiRoute.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
// orgs (admin routes)
r.Group("/orgs/:orgId", func() {
r.Get("/", wrap(GetOrgById))
r.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg))
r.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress))
r.Delete("/", wrap(DeleteOrgById))
r.Get("/users", wrap(GetOrgUsers))
r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
r.Delete("/users/:userId", wrap(RemoveOrgUser))
r.Get("/quotas", wrap(GetOrgQuotas))
r.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), wrap(UpdateOrgQuota))
apiRoute.Group("/orgs/:orgId", func(orgsRoute RouteRegister) {
orgsRoute.Get("/", wrap(GetOrgByID))
orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg))
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress))
orgsRoute.Delete("/", wrap(DeleteOrgByID))
orgsRoute.Get("/users", wrap(GetOrgUsers))
orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
orgsRoute.Delete("/users/:userId", wrap(RemoveOrgUser))
orgsRoute.Get("/quotas", wrap(GetOrgQuotas))
orgsRoute.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), wrap(UpdateOrgQuota))
}, reqGrafanaAdmin)
// orgs (admin routes)
r.Group("/orgs/name/:name", func() {
r.Get("/", wrap(GetOrgByName))
apiRoute.Group("/orgs/name/:name", func(orgsRoute RouteRegister) {
orgsRoute.Get("/", wrap(GetOrgByName))
}, reqGrafanaAdmin)
// auth api keys
r.Group("/auth/keys", func() {
r.Get("/", wrap(GetApiKeys))
r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
r.Delete("/:id", wrap(DeleteApiKey))
apiRoute.Group("/auth/keys", func(keysRoute RouteRegister) {
keysRoute.Get("/", wrap(GetAPIKeys))
keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey))
keysRoute.Delete("/:id", wrap(DeleteAPIKey))
}, reqOrgAdmin)
// Preferences
r.Group("/preferences", func() {
r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
apiRoute.Group("/preferences", func(prefRoute RouteRegister) {
prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
})
// Data sources
r.Group("/datasources", func() {
r.Get("/", wrap(GetDataSources))
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
r.Delete("/:id", DeleteDataSourceById)
r.Delete("/name/:name", DeleteDataSourceByName)
r.Get("/:id", wrap(GetDataSourceById))
r.Get("/name/:name", wrap(GetDataSourceByName))
apiRoute.Group("/datasources", func(datasourceRoute RouteRegister) {
datasourceRoute.Get("/", wrap(GetDataSources))
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource))
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
datasourceRoute.Delete("/:id", wrap(DeleteDataSourceByID))
datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName))
datasourceRoute.Get("/:id", wrap(GetDataSourceByID))
datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName))
}, reqOrgAdmin)
r.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn)
apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIDByName), reqSignedIn)
r.Get("/plugins", wrap(GetPluginList))
r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
apiRoute.Get("/plugins", wrap(GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID))
apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
r.Group("/plugins", func() {
r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
apiRoute.Group("/plugins", func(pluginRoute RouteRegister) {
pluginRoute.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
}, reqOrgAdmin)
r.Get("/frontend/settings/", GetFrontendSettings)
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest)
apiRoute.Get("/frontend/settings/", GetFrontendSettings)
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
r.Group("/dashboards", func() {
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
r.Get("/file/:file", GetDashboardFromJsonFile)
r.Get("/home", wrap(GetHomeDashboard))
r.Get("/tags", GetDashboardTags)
r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID))
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
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
r.Group("/dashboard/snapshots", func() {
r.Get("/", wrap(SearchDashboardSnapshots))
apiRoute.Group("/dashboard/snapshots", func(dashboardRoute RouteRegister) {
dashboardRoute.Get("/", wrap(SearchDashboardSnapshots))
})
// Playlist
r.Group("/playlists", func() {
r.Get("/", wrap(SearchPlaylists))
r.Get("/:id", ValidateOrgPlaylist, wrap(GetPlaylist))
r.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems))
r.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards))
r.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist))
r.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, wrap(UpdatePlaylist))
r.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), wrap(CreatePlaylist))
apiRoute.Group("/playlists", func(playlistRoute RouteRegister) {
playlistRoute.Get("/", wrap(SearchPlaylists))
playlistRoute.Get("/:id", ValidateOrgPlaylist, wrap(GetPlaylist))
playlistRoute.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems))
playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards))
playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist))
playlistRoute.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, wrap(UpdatePlaylist))
playlistRoute.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), wrap(CreatePlaylist))
})
// Search
r.Get("/search/", Search)
apiRoute.Get("/search/", Search)
// metrics
r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData))
apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk))
// metrics
r.Get("/metrics", wrap(GetInternalMetrics))
r.Group("/alerts", func() {
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
r.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole)
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
r.Get("/", wrap(GetAlerts))
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
apiRoute.Group("/alerts", func(alertsRoute RouteRegister) {
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
alertsRoute.Get("/", wrap(GetAlerts))
alertsRoute.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
})
r.Get("/alert-notifications", wrap(GetAlertNotifications))
r.Get("/alert-notifiers", wrap(GetAlertNotifiers))
apiRoute.Get("/alert-notifications", wrap(GetAlertNotifications))
apiRoute.Get("/alert-notifiers", wrap(GetAlertNotifiers))
r.Group("/alert-notifications", func() {
r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
r.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
r.Get("/:notificationId", wrap(GetAlertNotificationById))
r.Delete("/:notificationId", wrap(DeleteAlertNotification))
apiRoute.Group("/alert-notifications", func(alertNotifications RouteRegister) {
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationByID))
alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification))
}, reqEditorRole)
r.Get("/annotations", wrap(GetAnnotations))
r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
apiRoute.Get("/annotations", wrap(GetAnnotations))
apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
})
// error test
r.Get("/metrics/error", wrap(GenerateError))
@@ -284,16 +353,16 @@ func (hs *HttpServer) registerRoutes() {
}, reqSignedIn)
// admin api
r.Group("/api/admin", func() {
r.Get("/settings", AdminGetSettings)
r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
r.Delete("/users/:id", AdminDeleteUser)
r.Get("/users/:id/quotas", wrap(GetUserQuotas))
r.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota))
r.Get("/stats", AdminGetStats)
r.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), wrap(PauseAllAlerts))
r.Group("/api/admin", func(adminRoute RouteRegister) {
adminRoute.Get("/settings", AdminGetSettings)
adminRoute.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
adminRoute.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
adminRoute.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
adminRoute.Delete("/users/:id", AdminDeleteUser)
adminRoute.Get("/users/:id/quotas", wrap(GetUserQuotas))
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota))
adminRoute.Get("/stats", AdminGetStats)
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), wrap(PauseAllAlerts))
}, reqGrafanaAdmin)
// rendering
@@ -303,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)
@@ -312,7 +381,9 @@ func (hs *HttpServer) registerRoutes() {
// streams
//r.Post("/api/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream)
InitAppPluginRoutes(r)
r.Register(macaronR)
r.NotFound(NotFoundHandler)
InitAppPluginRoutes(macaronR)
macaronR.NotFound(NotFoundHandler)
}
+10 -11
View File
@@ -4,15 +4,14 @@ 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 {
return ApiError(500, "Failed to list api keys", err)
return Error(500, "Failed to list api keys", err)
}
result := make([]*m.ApiKeyDTO, len(query.Result))
@@ -24,25 +23,25 @@ func GetApiKeys(c *middleware.Context) Response {
}
}
return Json(200, result)
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}
err := bus.Dispatch(cmd)
if err != nil {
return ApiError(500, "Failed to delete API key", err)
return Error(500, "Failed to delete API key", err)
}
return ApiSuccess("API key deleted")
return Success("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)
return Error(400, "Invalid role specified", nil)
}
cmd.OrgId = c.OrgId
@@ -51,12 +50,12 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response {
cmd.Key = newKeyInfo.HashedKey
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to add API key", err)
return Error(500, "Failed to add API key", err)
}
result := &dtos.NewApiKeyResult{
Name: cmd.Result.Name,
Key: newKeyInfo.ClientSecret}
return Json(200, result)
return JSON(200, result)
}
+21 -16
View File
@@ -6,34 +6,39 @@ import (
"net/http"
"time"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/pluginproxy"
"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"
macaron "gopkg.in/macaron.v1"
)
var pluginProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
var pluginProxyTransport *http.Transport
func InitAppPluginRoutes(r *macaron.Macaron) {
pluginProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: setting.PluginAppsSkipVerifyTLS,
Renegotiation: tls.RenegotiateFreelyAsClient,
},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
for _, plugin := range plugins.Apps {
for _, route := range plugin.Routes {
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
handlers := make([]macaron.Handler, 0)
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
ReqSignedIn: true,
ReqGrafanaAdmin: route.ReqGrafanaAdmin,
ReqSignedIn: true,
}))
if route.ReqRole != "" {
@@ -50,11 +55,11 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
}
}
func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler {
return func(c *middleware.Context) {
func AppPluginRoute(route *plugins.AppPluginRoute, appID string) macaron.Handler {
return func(c *m.ReqContext) {
path := c.Params("*")
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId)
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID)
proxy.Transport = pluginProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request)
}
+33 -21
View File
@@ -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
@@ -65,7 +68,7 @@ func New(hash string) *Avatar {
return &Avatar{
hash: hash,
reqParams: url.Values{
"d": {"404"},
"d": {"retro"},
"size": {"200"},
"r": {"pg"}}.Encode(),
}
@@ -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{}
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)
}
@@ -217,7 +226,10 @@ func (this *thunderTask) Fetch() {
this.Done()
}
var client = &http.Client{}
var client *http.Client = &http.Client{
Timeout: time.Second * 2,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
func (this *thunderTask) fetch() error {
this.Avatar.timestamp = time.Now()
-513
View File
@@ -1,513 +0,0 @@
package cloudwatch
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
type actionHandler func(*cwRequest, *middleware.Context)
var actionHandlers map[string]actionHandler
type cwRequest struct {
Region string `json:"region"`
Action string `json:"action"`
Body []byte `json:"-"`
DataSource *m.DataSource
}
type datasourceInfo struct {
Profile string
Region string
AssumeRoleArn string
Namespace string
AccessKey string
SecretKey string
}
func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
accessKey := ""
secretKey := ""
for key, value := range req.DataSource.SecureJsonData.Decrypt() {
if key == "accessKey" {
accessKey = value
}
if key == "secretKey" {
secretKey = value
}
}
return &datasourceInfo{
AssumeRoleArn: assumeRoleArn,
Region: req.Region,
Profile: req.DataSource.Database,
AccessKey: accessKey,
SecretKey: secretKey,
}
}
func init() {
actionHandlers = map[string]actionHandler{
"GetMetricStatistics": handleGetMetricStatistics,
"ListMetrics": handleListMetrics,
"DescribeAlarms": handleDescribeAlarms,
"DescribeAlarmsForMetric": handleDescribeAlarmsForMetric,
"DescribeAlarmHistory": handleDescribeAlarmHistory,
"DescribeInstances": handleDescribeInstances,
"__GetRegions": handleGetRegions,
"__GetNamespaces": handleGetNamespaces,
"__GetMetrics": handleGetMetrics,
"__GetDimensions": handleGetDimensions,
}
}
type cache struct {
credential *credentials.Credentials
expiration *time.Time
}
var awsCredentialCache map[string]cache = make(map[string]cache)
var credentialCacheLock sync.RWMutex
func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
credentialCacheLock.RLock()
if _, ok := awsCredentialCache[cacheKey]; ok {
if awsCredentialCache[cacheKey].expiration != nil &&
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
result := awsCredentialCache[cacheKey].credential
credentialCacheLock.RUnlock()
return result, nil
}
}
credentialCacheLock.RUnlock()
accessKeyId := ""
secretAccessKey := ""
sessionToken := ""
var expiration *time.Time
expiration = nil
if strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 {
params := &sts.AssumeRoleInput{
RoleArn: aws.String(dsInfo.AssumeRoleArn),
RoleSessionName: aws.String("GrafanaSession"),
DurationSeconds: aws.Int64(900),
}
stsSess, err := session.NewSession()
if err != nil {
return nil, err
}
stsCreds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
remoteCredProvider(stsSess),
})
stsConfig := &aws.Config{
Region: aws.String(dsInfo.Region),
Credentials: stsCreds,
}
sess, err := session.NewSession(stsConfig)
if err != nil {
return nil, err
}
svc := sts.New(sess, stsConfig)
resp, err := svc.AssumeRole(params)
if err != nil {
return nil, err
}
if resp.Credentials != nil {
accessKeyId = *resp.Credentials.AccessKeyId
secretAccessKey = *resp.Credentials.SecretAccessKey
sessionToken = *resp.Credentials.SessionToken
expiration = resp.Credentials.Expiration
}
}
sess, err := session.NewSession()
if err != nil {
return nil, err
}
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{Value: credentials.Value{
AccessKeyID: accessKeyId,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
}},
&credentials.EnvProvider{},
&credentials.StaticProvider{Value: credentials.Value{
AccessKeyID: dsInfo.AccessKey,
SecretAccessKey: dsInfo.SecretKey,
}},
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
})
credentialCacheLock.Lock()
awsCredentialCache[cacheKey] = cache{
credential: creds,
expiration: expiration,
}
credentialCacheLock.Unlock()
return creds, nil
}
func remoteCredProvider(sess *session.Session) credentials.Provider {
ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
if len(ecsCredURI) > 0 {
return ecsCredProvider(sess, ecsCredURI)
}
return ec2RoleProvider(sess)
}
func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
const host = `169.254.170.2`
c := ec2metadata.New(sess)
return endpointcreds.NewProviderClient(
c.Client.Config,
c.Client.Handlers,
fmt.Sprintf("http://%s%s", host, uri),
func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
}
func ec2RoleProvider(sess *session.Session) credentials.Provider {
return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
}
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
creds, err := getCredentials(req.GetDatasourceInfo())
if err != nil {
return nil, err
}
cfg := &aws.Config{
Region: aws.String(req.Region),
Credentials: creds,
}
return cfg, nil
}
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
Statistics []*string `json:"statistics"`
ExtendedStatistics []*string `json:"extendedStatistics"`
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
Period int64 `json:"period"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &cloudwatch.GetMetricStatisticsInput{
Namespace: aws.String(reqParam.Parameters.Namespace),
MetricName: aws.String(reqParam.Parameters.MetricName),
Dimensions: reqParam.Parameters.Dimensions,
StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
Period: aws.Int64(reqParam.Parameters.Period),
}
if len(reqParam.Parameters.Statistics) != 0 {
params.Statistics = reqParam.Parameters.Statistics
}
if len(reqParam.Parameters.ExtendedStatistics) != 0 {
params.ExtendedStatistics = reqParam.Parameters.ExtendedStatistics
}
resp, err := svc.GetMetricStatistics(params)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc(1)
c.JSON(200, resp)
}
func handleListMetrics(req *cwRequest, c *middleware.Context) {
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions []*cloudwatch.DimensionFilter `json:"dimensions"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &cloudwatch.ListMetricsInput{
Namespace: aws.String(reqParam.Parameters.Namespace),
MetricName: aws.String(reqParam.Parameters.MetricName),
Dimensions: reqParam.Parameters.Dimensions,
}
var resp cloudwatch.ListMetricsOutput
err = svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
for _, metric := range metrics {
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
}
return !lastPage
})
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
c.JSON(200, resp)
}
func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
ActionPrefix string `json:"actionPrefix"`
AlarmNamePrefix string `json:"alarmNamePrefix"`
AlarmNames []*string `json:"alarmNames"`
StateValue string `json:"stateValue"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &cloudwatch.DescribeAlarmsInput{
MaxRecords: aws.Int64(100),
}
if reqParam.Parameters.ActionPrefix != "" {
params.ActionPrefix = aws.String(reqParam.Parameters.ActionPrefix)
}
if reqParam.Parameters.AlarmNamePrefix != "" {
params.AlarmNamePrefix = aws.String(reqParam.Parameters.AlarmNamePrefix)
}
if len(reqParam.Parameters.AlarmNames) != 0 {
params.AlarmNames = reqParam.Parameters.AlarmNames
}
if reqParam.Parameters.StateValue != "" {
params.StateValue = aws.String(reqParam.Parameters.StateValue)
}
resp, err := svc.DescribeAlarms(params)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
c.JSON(200, resp)
}
func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
Statistic string `json:"statistic"`
ExtendedStatistic string `json:"extendedStatistic"`
Period int64 `json:"period"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &cloudwatch.DescribeAlarmsForMetricInput{
Namespace: aws.String(reqParam.Parameters.Namespace),
MetricName: aws.String(reqParam.Parameters.MetricName),
Period: aws.Int64(reqParam.Parameters.Period),
}
if len(reqParam.Parameters.Dimensions) != 0 {
params.Dimensions = reqParam.Parameters.Dimensions
}
if reqParam.Parameters.Statistic != "" {
params.Statistic = aws.String(reqParam.Parameters.Statistic)
}
if reqParam.Parameters.ExtendedStatistic != "" {
params.ExtendedStatistic = aws.String(reqParam.Parameters.ExtendedStatistic)
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
c.JSON(200, resp)
}
func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
AlarmName string `json:"alarmName"`
HistoryItemType string `json:"historyItemType"`
StartDate int64 `json:"startDate"`
EndDate int64 `json:"endDate"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &cloudwatch.DescribeAlarmHistoryInput{
AlarmName: aws.String(reqParam.Parameters.AlarmName),
StartDate: aws.Time(time.Unix(reqParam.Parameters.StartDate, 0)),
EndDate: aws.Time(time.Unix(reqParam.Parameters.EndDate, 0)),
}
if reqParam.Parameters.HistoryItemType != "" {
params.HistoryItemType = aws.String(reqParam.Parameters.HistoryItemType)
}
resp, err := svc.DescribeAlarmHistory(params)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
c.JSON(200, resp)
}
func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := ec2.New(sess, cfg)
reqParam := &struct {
Parameters struct {
Filters []*ec2.Filter `json:"filters"`
InstanceIds []*string `json:"instanceIds"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &ec2.DescribeInstancesInput{}
if len(reqParam.Parameters.Filters) > 0 {
params.Filters = reqParam.Parameters.Filters
}
if len(reqParam.Parameters.InstanceIds) > 0 {
params.InstanceIds = reqParam.Parameters.InstanceIds
}
var resp ec2.DescribeInstancesOutput
err = svc.DescribeInstancesPages(params,
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
for _, reservation := range reservations {
resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation))
}
return !lastPage
})
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
c.JSON(200, resp)
}
func HandleRequest(c *middleware.Context, ds *m.DataSource) {
var req cwRequest
req.Body, _ = ioutil.ReadAll(c.Req.Request.Body)
req.DataSource = ds
json.Unmarshal(req.Body, &req)
if handler, found := actionHandlers[req.Action]; !found {
c.JsonApiErr(500, "Unexpected AWS Action", errors.New(req.Action))
return
} else {
handler(&req, c)
}
}
-69
View File
@@ -1,69 +0,0 @@
package cloudwatch
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
. "github.com/smartystreets/goconvey/convey"
)
func TestCloudWatchMetrics(t *testing.T) {
Convey("When calling getMetricsForCustomMetrics", t, func() {
dsInfo := &datasourceInfo{
Region: "us-east-1",
Namespace: "Foo",
Profile: "default",
AssumeRoleArn: "",
}
f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
MetricName: aws.String("Test_MetricName"),
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("Test_DimensionName"),
},
},
},
},
}, nil
}
metrics, _ := getMetricsForCustomMetrics(dsInfo, f)
Convey("Should contain Test_MetricName", func() {
So(metrics, ShouldContain, "Test_MetricName")
})
})
Convey("When calling getDimensionsForCustomMetrics", t, func() {
dsInfo := &datasourceInfo{
Region: "us-east-1",
Namespace: "Foo",
Profile: "default",
AssumeRoleArn: "",
}
f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
MetricName: aws.String("Test_MetricName"),
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("Test_DimensionName"),
},
},
},
},
}, nil
}
dimensionKeys, _ := getDimensionsForCustomMetrics(dsInfo, f)
Convey("Should contain Test_DimensionName", func() {
So(dimensionKeys, ShouldContain, "Test_DimensionName")
})
})
}
+17 -13
View File
@@ -4,22 +4,22 @@ 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"
)
var (
NotFound = func() Response {
return ApiError(404, "Not found", nil)
return Error(404, "Not found", nil)
}
ServerError = func(err error) Response {
return ApiError(500, "Server error", err)
return Error(500, "Server error", err)
}
)
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)
}
@@ -67,22 +67,25 @@ func (r *NormalResponse) Header(key, value string) *NormalResponse {
return r
}
// functions to create responses
// Empty create an empty response
func Empty(status int) *NormalResponse {
return Respond(status, nil)
}
func Json(status int, body interface{}) *NormalResponse {
// JSON create a JSON response
func JSON(status int, body interface{}) *NormalResponse {
return Respond(status, body).Header("Content-Type", "application/json")
}
func ApiSuccess(message string) *NormalResponse {
// Success create a successful response
func Success(message string) *NormalResponse {
resp := make(map[string]interface{})
resp["message"] = message
return Json(200, resp)
return JSON(200, resp)
}
func ApiError(status int, message string, err error) *NormalResponse {
// Error create a erroneous response
func Error(status int, message string, err error) *NormalResponse {
data := make(map[string]interface{})
switch status {
@@ -102,7 +105,7 @@ func ApiError(status int, message string, err error) *NormalResponse {
}
}
resp := Json(status, data)
resp := JSON(status, data)
if err != nil {
resp.errMessage = message
@@ -112,6 +115,7 @@ func ApiError(status int, message string, err error) *NormalResponse {
return resp
}
// Respond create a response
func Respond(status int, body interface{}) *NormalResponse {
var b []byte
var err error
@@ -122,7 +126,7 @@ func Respond(status int, body interface{}) *NormalResponse {
b = []byte(t)
default:
if b, err = json.Marshal(body); err != nil {
return ApiError(500, "body json marshal", err)
return Error(500, "body json marshal", err)
}
}
return &NormalResponse{
+105
View File
@@ -0,0 +1,105 @@
package api
import (
"net/http"
"net/http/httptest"
"path/filepath"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"gopkg.in/macaron.v1"
. "github.com/smartystreets/goconvey/convey"
)
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
loggedInUserScenarioWithRole(desc, "GET", url, url, m.ROLE_EDITOR, fn)
}
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role m.RoleType, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
}
return nil
})
switch method {
case "GET":
sc.m.Get(routePattern, sc.defaultHandler)
case "DELETE":
sc.m.Delete(routePattern, sc.defaultHandler)
}
fn(sc)
})
}
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
So(err, ShouldBeNil)
sc.req = req
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
So(err, ShouldBeNil)
sc.req = req
return sc
}
type scenarioContext struct {
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
}
func (sc *scenarioContext) exec() {
sc.m.ServeHTTP(sc.resp, sc.req)
}
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *m.ReqContext) Response
func setupScenarioContext(url string) *scenarioContext {
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
return sc
}
+343 -117
View File
@@ -2,30 +2,32 @@ package api
import (
"encoding/json"
"fmt"
"os"
"path"
"strings"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/dashdiffs"
"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
}
query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashId}
query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashID}
if err := bus.Dispatch(&query); err != nil {
return false, err
}
@@ -33,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 Error(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 Error(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 Error(500, "Error while checking if dashboard was starred by user", err)
}
// Finding creator and last updater of the dashboard
updater, creator := "Anonymous", "Anonymous"
@@ -60,99 +72,180 @@ 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 Error(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,
}
c.TimeRequest(metrics.M_Api_Dashboard_Get)
c.JSON(200, dto)
return JSON(200, dto)
}
func getUserLogin(userId int64) string {
query := m.GetUserByIdQuery{Id: userId}
func getUserLogin(userID int64) string {
query := m.GetUserByIdQuery{Id: userID}
err := bus.Dispatch(&query)
if err != nil {
return "Anonymous"
} else {
user := query.Result
return user.Login
}
return query.Result.Login
}
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, Error(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 {
cmd.OrgId = c.OrgId
func DeleteDashboard(c *m.ReqContext) Response {
query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
if !c.IsSignedIn {
cmd.UserId = -1
} else {
cmd.UserId = c.UserId
if err := bus.Dispatch(&query); err != nil {
return Error(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 Error(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 Error(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)
return Error(500, "failed to get quota", err)
}
if limitReached {
return ApiError(403, "Quota reached", nil)
return Error(403, "Quota reached", nil)
}
}
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 {
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
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 Error(400, err.Error(), nil)
}
if err == m.ErrDashboardUpdateAccessDenied {
return Error(403, err.Error(), err)
}
if err == m.ErrDashboardContainsInvalidAlertData {
return Error(500, "Invalid alert data. Cannot save dashboard", err)
}
err := bus.Dispatch(&cmd)
if err != nil {
if err == m.ErrDashboardWithSameNameExists {
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
if err == m.ErrDashboardWithSameNameInFolderExists {
return JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()})
}
if err == m.ErrDashboardVersionMismatch {
return Json(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
return JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
}
if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok {
message := "The dashboard belongs to plugin " + pluginErr.PluginId + "."
@@ -160,102 +253,235 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist {
message = "The dashboard belongs to plugin " + pluginDef.Name + "."
}
return Json(412, util.DynMap{"status": "plugin-dashboard", "message": message})
return JSON(412, util.DynMap{"status": "plugin-dashboard", "message": message})
}
if err == m.ErrDashboardNotFound {
return Json(404, util.DynMap{"status": "not-found", "message": err.Error()})
return JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
}
return ApiError(500, "Failed to save dashboard", err)
return Error(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 Error(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)
return Error(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}
return Json(200, &dashRedirect)
} else {
log.Warn("Failed to get slug from database, %s", err.Error())
url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
return JSON(200, &dashRedirect)
}
log.Warn("Failed to get slug from database, %s", err.Error())
}
filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
file, err := os.Open(filePath)
if err != nil {
return ApiError(500, "Failed to load home dashboard", err)
return Error(500, "Failed to load home dashboard", err)
}
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)
return Error(500, "Failed to load home dashboard", err)
}
if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
addGettingStartedPanelToHomeDashboard(dash.Dashboard)
}
return Json(200, &dash)
return JSON(200, &dash)
}
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)
dash.Set("panels", panels)
}
func GetDashboardFromJsonFile(c *middleware.Context) {
file := c.Params(":file")
// GetDashboardVersions returns all dashboard versions as JSON
func GetDashboardVersions(c *m.ReqContext) Response {
dashID := c.ParamsInt64(":dashboardId")
dashboard := search.GetDashboardFromJsonIndex(file)
if dashboard == nil {
c.JsonApiErr(404, "Dashboard not found", nil)
return
guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
dash := dtos.DashboardFullWithMeta{Dashboard: dashboard.Data}
dash.Meta.Type = m.DashTypeJson
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
query := m.GetDashboardVersionsQuery{
OrgId: c.OrgId,
DashboardId: dashID,
Limit: c.QueryInt("limit"),
Start: c.QueryInt("start"),
}
c.JSON(200, &dash)
if err := bus.Dispatch(&query); err != nil {
return Error(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err)
}
for _, version := range query.Result {
if version.RestoredFrom == version.Version {
version.Message = "Initial save (created by migration)"
continue
}
if version.RestoredFrom > 0 {
version.Message = fmt.Sprintf("Restored from version %d", version.RestoredFrom)
continue
}
if version.ParentVersion == 0 {
version.Message = "Initial save"
}
}
return JSON(200, query.Result)
}
func GetDashboardTags(c *middleware.Context) {
// GetDashboardVersion returns the dashboard version with the given 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: dashID,
Version: c.ParamsInt(":id"),
}
if err := bus.Dispatch(&query); err != nil {
return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
}
creator := "Anonymous"
if query.Result.CreatedBy > 0 {
creator = getUserLogin(query.Result.CreatedBy)
}
dashVersionMeta := &m.DashboardVersionMeta{
DashboardVersion: *query.Result,
CreatedBy: creator,
}
return JSON(200, dashVersionMeta)
}
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
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,
DiffType: dashdiffs.ParseDiffType(apiOptions.DiffType),
Base: dashdiffs.DiffTarget{
DashboardId: apiOptions.Base.DashboardId,
Version: apiOptions.Base.Version,
UnsavedDashboard: apiOptions.Base.UnsavedDashboard,
},
New: dashdiffs.DiffTarget{
DashboardId: apiOptions.New.DashboardId,
Version: apiOptions.New.Version,
UnsavedDashboard: apiOptions.New.UnsavedDashboard,
},
}
result, err := dashdiffs.CalculateDiff(&options)
if err != nil {
if err == m.ErrDashboardVersionNotFound {
return Error(404, "Dashboard version not found", err)
}
return Error(500, "Unable to compute diff", err)
}
if options.DiffType == dashdiffs.DiffDelta {
return Respond(200, result.Delta).Header("Content-Type", "application/json")
}
return Respond(200, result.Delta).Header("Content-Type", "text/html")
}
// RestoreDashboardVersion restores a dashboard to the given version.
func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
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)
}
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
if err := bus.Dispatch(&versionQuery); err != nil {
return Error(404, "Dashboard version not found", nil)
}
version := versionQuery.Result
saveCmd := m.SaveDashboardCommand{}
saveCmd.RestoredFrom = version.Version
saveCmd.OrgId = c.OrgId
saveCmd.UserId = c.UserId
saveCmd.Dashboard = version.Data
saveCmd.Dashboard.Set("version", dash.Version)
saveCmd.Dashboard.Set("uid", dash.Uid)
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
return PostDashboard(c, saveCmd)
}
func GetDashboardTags(c *m.ReqContext) {
query := m.GetDashboardTagsQuery{OrgId: c.OrgId}
err := bus.Dispatch(&query)
if err != nil {
+90
View File
@@ -0,0 +1,90 @@
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetDashboardPermissionList(c *m.ReqContext) Response {
dashID := c.ParamsInt64(":dashboardId")
_, rsp := getDashboardHelper(c.OrgId, "", dashID, "")
if rsp != nil {
return rsp
}
g := guardian.New(dashID, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
acl, err := g.GetAcl()
if err != nil {
return Error(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 Error(400, err.Error(), err)
}
return Error(500, "Error while checking dashboard permissions", err)
}
return Error(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 Error(409, err.Error(), err)
}
return Error(500, "Failed to create permission", err)
}
return Success("Dashboard permissions updated")
}
+209
View File
@@ -0,0 +1,209 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardPermissionApiEndpoint(t *testing.T) {
Convey("Dashboard permissions test", t, func() {
Convey("Given dashboard not exists", func() {
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
return m.ErrDashboardNotFound
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
})
Convey("Given user has no admin permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("Given user has admin permissions and permissions to update", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: true,
GetAclValue: []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
},
})
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
callGetDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 5)
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("When trying to update permissions with duplicate permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
})
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("When trying to override inherited permissions with lower presedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
)
getDashboardQueryResult := m.NewDashboard("Dash")
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = getDashboardQueryResult
return nil
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) {
callUpdateDashboardPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
})
}
func callGetDashboardPermissions(sc *scenarioContext) {
sc.handlerFunc = GetDashboardPermissionList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func callUpdateDashboardPermissions(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func updateDashboardPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.OrgId = TestOrgID
sc.context.UserId = TestUserID
return UpdateDashboardPermissions(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
+44 -17
View File
@@ -6,13 +6,13 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func GetSharingOptions(c *middleware.Context) {
func GetSharingOptions(c *m.ReqContext) {
c.JSON(200, util.DynMap{
"externalSnapshotURL": setting.ExternalSnapshotUrl,
"externalSnapshotName": setting.ExternalSnapshotName,
@@ -20,7 +20,7 @@ func GetSharingOptions(c *middleware.Context) {
})
}
func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotCommand) {
if cmd.Name == "" {
cmd.Name = "Unnamed snapshot"
}
@@ -34,13 +34,13 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho
cmd.OrgId = -1
cmd.UserId = -1
metrics.M_Api_Dashboard_Snapshot_External.Inc(1)
metrics.M_Api_Dashboard_Snapshot_External.Inc()
} else {
cmd.Key = util.GetRandomString(32)
cmd.DeleteKey = util.GetRandomString(32)
cmd.OrgId = c.OrgId
cmd.UserId = c.UserId
metrics.M_Api_Dashboard_Snapshot_Create.Inc(1)
metrics.M_Api_Dashboard_Snapshot_Create.Inc()
}
if err := bus.Dispatch(&cmd); err != nil {
@@ -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}
@@ -84,25 +85,50 @@ func GetDashboardSnapshot(c *middleware.Context) {
},
}
metrics.M_Api_Dashboard_Snapshot_Get.Inc(1)
metrics.M_Api_Dashboard_Snapshot_Get.Inc()
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
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 Error(500, "Failed to get dashboard snapshot", err)
}
if query.Result == nil {
return Error(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 Error(500, "Error while checking permissions for snapshot", err)
}
if !canEdit && query.Result.UserId != c.SignedInUser.UserId {
return Error(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 Error(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,14 +137,15 @@ 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)
if err != nil {
return ApiError(500, "Search failed", err)
return Error(500, "Search failed", err)
}
dtos := make([]*m.DashboardSnapshotDTO, len(searchQuery.Result))
@@ -138,5 +165,5 @@ func SearchDashboardSnapshots(c *middleware.Context) Response {
}
}
return Json(200, dtos)
return JSON(200, dtos)
}
+97
View File
@@ -0,0 +1,97 @@
package api
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardSnapshotApiEndpoint(t *testing.T) {
Convey("Given a single snapshot", t, func() {
jsonModel, _ := simplejson.NewJson([]byte(`{"id":100}`))
mockSnapshotResult := &m.DashboardSnapshot{
Id: 1,
Dashboard: jsonModel,
Expires: time.Now().Add(time.Duration(1000) * time.Second),
UserId: 999999,
}
bus.AddHandler("test", func(query *m.GetDashboardSnapshotQuery) error {
query.Result = mockSnapshotResult
return nil
})
bus.AddHandler("test", func(cmd *m.DeleteDashboardSnapshotCommand) error {
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
aclMockResp := []*m.DashboardAclInfoDTO{}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
teamResp := []*m.Team{}
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = teamResp
return nil
})
Convey("When user has editor role and is not in the ACL", func() {
Convey("Should not be able to delete snapshot", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is editor and dashboard has default ACL", func() {
aclMockResp = []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
}
Convey("Should be able to delete a snapshot", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
})
})
})
Convey("When user is editor and is the creator of the snapshot", func() {
aclMockResp = []*m.DashboardAclInfoDTO{}
mockSnapshotResult.UserId = TestUserID
Convey("Should be able to delete a snapshot", func() {
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
})
})
})
})
}
+911
View 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
}
+23 -131
View File
@@ -1,167 +1,59 @@
package api
import (
"bytes"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"fmt"
"time"
"github.com/grafana/grafana/pkg/api/cloudwatch"
"github.com/grafana/grafana/pkg/api/pluginproxy"
"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/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/plugins"
)
var (
dataproxyLogger log.Logger = log.New("data-proxy-log")
)
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
director := func(req *http.Request) {
req.URL.Scheme = targetUrl.Scheme
req.URL.Host = targetUrl.Host
req.Host = targetUrl.Host
func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) {
cacheKey := fmt.Sprintf("ds-%d", id)
reqQueryVals := req.URL.Query()
if ds.Type == m.DS_INFLUXDB_08 {
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, "db/"+ds.Database+"/"+proxyPath)
reqQueryVals.Add("u", ds.User)
reqQueryVals.Add("p", ds.Password)
req.URL.RawQuery = reqQueryVals.Encode()
} else if ds.Type == m.DS_INFLUXDB {
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
req.URL.RawQuery = reqQueryVals.Encode()
if !ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.User, ds.Password))
if !nocache {
if cached, found := hs.cache.Get(cacheKey); found {
ds := cached.(*m.DataSource)
if ds.OrgId == orgID {
return ds, nil
}
} else {
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
}
if ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword))
}
dsAuth := req.Header.Get("X-DS-Authorization")
if len(dsAuth) > 0 {
req.Header.Del("X-DS-Authorization")
req.Header.Del("Authorization")
req.Header.Add("Authorization", dsAuth)
}
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")
}
return &httputil.ReverseProxy{Director: director, FlushInterval: time.Millisecond * 200}
}
func getDatasource(id int64, orgId int64) (*m.DataSource, error) {
query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgId}
query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
hs.cache.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}
func ProxyDataSourceRequest(c *middleware.Context) {
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
ds, err := getDatasource(c.ParamsInt64(":id"), c.OrgId)
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache)
if err != nil {
c.JsonApiErr(500, "Unable to load datasource meta data", err)
return
}
if ds.Type == m.DS_CLOUDWATCH {
cloudwatch.HandleRequest(c, ds)
// find plugin
plugin, ok := plugins.DataSources[ds.Type]
if !ok {
c.JsonApiErr(500, "Unable to find datasource plugin", err)
return
}
if ds.Type == m.DS_INFLUXDB {
if c.Query("db") != ds.Database {
c.JsonApiErr(403, "Datasource is not configured to allow this database", nil)
return
}
}
targetUrl, _ := url.Parse(ds.Url)
if len(setting.DataProxyWhiteList) > 0 {
if _, exists := setting.DataProxyWhiteList[targetUrl.Host]; !exists {
c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil)
return
}
}
proxyPath := c.Params("*")
if ds.Type == m.DS_PROMETHEUS {
if c.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxyPath, "api/") {
c.JsonApiErr(403, "GET is only allowed on proxied Prometheus datasource", nil)
return
}
}
if ds.Type == m.DS_ES {
if c.Req.Request.Method == "DELETE" {
c.JsonApiErr(403, "Deletes not allowed on proxied Elasticsearch datasource", nil)
return
}
if c.Req.Request.Method == "PUT" {
c.JsonApiErr(403, "Puts not allowed on proxied Elasticsearch datasource", nil)
return
}
if c.Req.Request.Method == "POST" && proxyPath != "_msearch" {
c.JsonApiErr(403, "Posts not allowed on proxied Elasticsearch datasource except on /_msearch", nil)
return
}
}
proxy := NewReverseProxy(ds, proxyPath, targetUrl)
proxy.Transport, err = ds.GetHttpTransport()
if err != nil {
c.JsonApiErr(400, "Unable to load TLS certificate", err)
return
}
logProxyRequest(ds.Type, c)
proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie")
}
func logProxyRequest(dataSourceType string, c *middleware.Context) {
if !setting.DataProxyLogging {
return
}
var body string
if c.Req.Request.Body != nil {
buffer, err := ioutil.ReadAll(c.Req.Request.Body)
if err == nil {
c.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
body = string(buffer)
}
}
dataproxyLogger.Info("Proxying incoming request",
"userid", c.UserId,
"orgid", c.OrgId,
"username", c.Login,
"datasource", dataSourceType,
"uri", c.Req.RequestURI,
"method", c.Req.Request.Method,
"body", body)
proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
proxy.HandleRequest()
}
-63
View File
@@ -1,63 +0,0 @@
package api
import (
"net/http"
"net/url"
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestDataSourceProxy(t *testing.T) {
Convey("When getting graphite datasource proxy", t, func() {
ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
targetUrl, err := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "/render", targetUrl)
proxy.Transport, err = ds.GetHttpTransport()
So(err, ShouldBeNil)
transport, ok := proxy.Transport.(*http.Transport)
So(ok, ShouldBeTrue)
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldBeTrue)
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
proxy.Director(&req)
Convey("Can translate request url and path", func() {
So(req.URL.Host, ShouldEqual, "graphite:8080")
So(req.URL.Path, ShouldEqual, "/render")
})
})
Convey("When getting influxdb datasource proxy", t, func() {
ds := m.DataSource{
Type: m.DS_INFLUXDB_08,
Url: "http://influxdb:8083",
Database: "site",
User: "user",
Password: "password",
}
targetUrl, _ := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "", targetUrl)
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
proxy.Director(&req)
Convey("Should add db to url", func() {
So(req.URL.Path, ShouldEqual, "/db/site/")
})
Convey("Should add username and password", func() {
queryVals := req.URL.Query()
So(queryVals["u"][0], ShouldEqual, "user")
So(queryVals["p"][0], ShouldEqual, "password")
})
})
}
+81 -50
View File
@@ -5,22 +5,21 @@ 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 {
return ApiError(500, "Failed to query datasources", err)
return Error(500, "Failed to query datasources", err)
}
result := make(dtos.DataSourceList, 0)
for _, ds := range query.Result {
dsItem := dtos.DataSource{
dsItem := dtos.DataSourceListItemDTO{
Id: ds.Id,
OrgId: ds.OrgId,
Name: ds.Name,
@@ -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 {
@@ -46,10 +46,10 @@ func GetDataSources(c *middleware.Context) Response {
sort.Sort(result)
return Json(200, &result)
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,
@@ -57,101 +57,129 @@ func GetDataSourceById(c *middleware.Context) Response {
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrDataSourceNotFound {
return ApiError(404, "Data source not found", nil)
return Error(404, "Data source not found", nil)
}
return ApiError(500, "Failed to query datasources", err)
return Error(500, "Failed to query datasources", err)
}
ds := query.Result
dtos := convertModelToDtos(ds)
return Json(200, &dtos)
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 Error(400, "Missing valid datasource id", nil)
}
ds, err := getRawDataSourceByID(id, c.OrgId)
if err != nil {
return Error(400, "Failed to delete datasource", nil)
}
if ds.ReadOnly {
return Error(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 Error(500, "Failed to delete datasource", err)
}
c.JsonOK("Data source deleted")
return Success("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 Error(400, "Missing valid datasource name", nil)
}
getCmd := &m.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
if err := bus.Dispatch(getCmd); err != nil {
return Error(500, "Failed to delete datasource", err)
}
if getCmd.Result.ReadOnly {
return Error(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 Error(500, "Failed to delete datasource", err)
}
c.JsonOK("Data source deleted")
return Success("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 Error(409, err.Error(), err)
}
c.JsonApiErr(500, "Failed to add datasource", err)
return
return Error(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")
err := fillWithSecureJsonData(&cmd)
err := fillWithSecureJSONData(&cmd)
if err != nil {
return ApiError(500, "Failed to update datasource", err)
return Error(500, "Failed to update datasource", err)
}
err = bus.Dispatch(&cmd)
if err != nil {
return ApiError(500, "Failed to update datasource", err)
if err == m.ErrDataSourceUpdatingOldVersion {
return Error(500, "Failed to update datasource. Reload new version and try again", err)
}
return Error(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 {
func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
if len(cmd.SecureJsonData) == 0 {
return nil
}
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId)
if err != nil {
return err
}
secureJsonData := ds.SecureJsonData.Decrypt()
for k, v := range secureJsonData {
if ds.ReadOnly {
return m.ErrDatasourceIsReadOnly
}
secureJSONData := ds.SecureJsonData.Decrypt()
for k, v := range secureJSONData {
if _, ok := cmd.SecureJsonData[k]; !ok {
cmd.SecureJsonData[k] = v
@@ -161,10 +189,10 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
return nil
}
func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) {
func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) {
query := m.GetDataSourceByIdQuery{
Id: id,
OrgId: orgId,
OrgId: orgID,
}
if err := bus.Dispatch(&query); err != nil {
@@ -175,29 +203,30 @@ 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 {
if err == m.ErrDataSourceNotFound {
return ApiError(404, "Data source not found", nil)
return Error(404, "Data source not found", nil)
}
return ApiError(500, "Failed to query datasources", err)
return Error(500, "Failed to query datasources", err)
}
dtos := convertModelToDtos(query.Result)
return Json(200, &dtos)
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 {
if err == m.ErrDataSourceNotFound {
return ApiError(404, "Data source not found", nil)
return Error(404, "Data source not found", nil)
}
return ApiError(500, "Failed to query datasources", err)
return Error(500, "Failed to query datasources", err)
}
ds := query.Result
@@ -205,7 +234,7 @@ func GetDataSourceIdByName(c *middleware.Context) Response {
Id: ds.Id,
}
return Json(200, &dtos)
return JSON(200, &dtos)
}
func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
@@ -226,6 +255,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 {
-82
View File
@@ -2,17 +2,11 @@ package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/models"
macaron "gopkg.in/macaron.v1"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
. "github.com/smartystreets/goconvey/convey"
)
@@ -54,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
View 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"`
}
+2 -1
View File
@@ -19,7 +19,8 @@ type AlertRule struct {
EvalDate time.Time `json:"evalDate"`
EvalData *simplejson.Json `json:"evalData"`
ExecutionError string `json:"executionError"`
DashbboardUri string `json:"dashboardUri"`
Url string `json:"url"`
CanEdit bool `json:"canEdit"`
}
type AlertNotification struct {
+29 -14
View File
@@ -2,22 +2,37 @@ package dtos
import "github.com/grafana/grafana/pkg/components/simplejson"
type Annotation struct {
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
NewState string `json:"newState"`
PrevState string `json:"prevState"`
Time int64 `json:"time"`
Title string `json:"title"`
Text string `json:"text"`
Metric string `json:"metric"`
type PostAnnotationsCmd struct {
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
Time int64 `json:"time"`
Text string `json:"text"`
Tags []string `json:"tags"`
Data *simplejson.Json `json:"data"`
IsRegion bool `json:"isRegion"`
TimeEnd int64 `json:"timeEnd"`
}
Data *simplejson.Json `json:"data"`
type UpdateAnnotationsCmd struct {
Id int64 `json:"id"`
Time int64 `json:"time"`
Text string `json:"text"`
Tags []string `json:"tags"`
IsRegion bool `json:"isRegion"`
TimeEnd int64 `json:"timeEnd"`
}
type DeleteAnnotationsCmd struct {
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
AnnotationId int64 `json:"annotationId"`
RegionId int64 `json:"regionId"`
}
type PostGraphiteAnnotationsCmd struct {
When int64 `json:"when"`
What string `json:"what"`
Data string `json:"data"`
Tags interface{} `json:"tags"`
}
+56
View File
@@ -0,0 +1,56 @@
package dtos
import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
)
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"`
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 {
Meta DashboardMeta `json:"meta"`
Dashboard *simplejson.Json `json:"dashboard"`
}
type DashboardRedirect struct {
RedirectUri string `json:"redirectUri"`
}
type CalculateDiffOptions struct {
Base CalculateDiffTarget `json:"base" binding:"Required"`
New CalculateDiffTarget `json:"new" binding:"Required"`
DiffType string `json:"diffType" binding:"Required"`
}
type CalculateDiffTarget struct {
DashboardId int64 `json:"dashboardId"`
Version int `json:"version"`
UnsavedDashboard *simplejson.Json `json:"unsavedDashboard"`
}
type RestoreDashboardVersionCommand struct {
Version int `json:"version" binding:"Required"`
}
+61
View 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
View File
@@ -0,0 +1,25 @@
package dtos
import "time"
type Folder struct {
Id int64 `json:"id"`
Uid string `json:"uid"`
Title string `json:"title"`
Url string `json:"url"`
HasAcl bool `json:"hasAcl"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CreatedBy string `json:"createdBy"`
Created time.Time `json:"created"`
UpdatedBy string `json:"updatedBy"`
Updated time.Time `json:"updated"`
Version int `json:"version"`
}
type FolderSearchHit struct {
Id int64 `json:"id"`
Uid string `json:"uid"`
Title string `json:"title"`
}
+14 -7
View File
@@ -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"`
}
+1 -1
View File
@@ -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 {
+22 -61
View File
@@ -3,8 +3,8 @@ package dtos
import (
"crypto/md5"
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
@@ -28,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"`
@@ -38,66 +39,6 @@ type CurrentUser struct {
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
}
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"`
}
type DashboardFullWithMeta struct {
Meta DashboardMeta `json:"meta"`
Dashboard *simplejson.Json `json:"dashboard"`
}
type DashboardRedirect struct {
RedirectUri string `json:"redirectUri"`
}
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 DataSourceList []DataSource
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"`
@@ -109,6 +50,10 @@ type UserStars struct {
}
func GetGravatarUrl(text string) string {
if setting.DisableGravatar {
return "/public/img/user_profile.png"
}
if text == "" {
return ""
}
@@ -117,3 +62,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)
}
+2
View File
@@ -21,6 +21,7 @@ type PluginSetting struct {
LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"`
State string `json:"state"`
}
type PluginListItem struct {
@@ -33,6 +34,7 @@ type PluginListItem struct {
LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"`
DefaultNavUrl string `json:"defaultNavUrl"`
State string `json:"state"`
}
type PluginList []PluginListItem
+2 -2
View File
@@ -2,12 +2,12 @@ package dtos
type Prefs struct {
Theme string `json:"theme"`
HomeDashboardId int64 `json:"homeDashboardId"`
HomeDashboardID int64 `json:"homeDashboardId"`
Timezone string `json:"timezone"`
}
type UpdatePrefsCmd struct {
Theme string `json:"theme"`
HomeDashboardId int64 `json:"homeDashboardId"`
HomeDashboardID int64 `json:"homeDashboardId"`
Timezone string `json:"timezone"`
}
+1 -1
View File
@@ -31,7 +31,7 @@ type AdminUpdateUserPasswordForm struct {
}
type AdminUpdateUserPermissionsForm struct {
IsGrafanaAdmin bool `json:"isGrafanaAdmin" binding:"Required"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
}
type AdminUserListItem struct {
+146
View File
@@ -0,0 +1,146 @@
package api
import (
"fmt"
"github.com/grafana/grafana/pkg/api/dtos"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
func GetFolders(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folders, err := s.GetFolders(c.QueryInt("limit"))
if err != nil {
return toFolderError(err)
}
result := make([]dtos.FolderSearchHit, 0)
for _, f := range folders {
result = append(result, dtos.FolderSearchHit{
Id: f.Id,
Uid: f.Uid,
Title: f.Title,
})
}
return JSON(200, result)
}
func GetFolderByUID(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUID(c.Params(":uid"))
if err != nil {
return toFolderError(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
return JSON(200, toFolderDto(g, folder))
}
func GetFolderByID(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByID(c.ParamsInt64(":id"))
if err != nil {
return toFolderError(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
return JSON(200, toFolderDto(g, folder))
}
func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
err := s.CreateFolder(&cmd)
if err != nil {
return toFolderError(err)
}
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
return JSON(200, toFolderDto(g, cmd.Result))
}
func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
err := s.UpdateFolder(c.Params(":uid"), &cmd)
if err != nil {
return toFolderError(err)
}
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
return JSON(200, toFolderDto(g, cmd.Result))
}
func DeleteFolder(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
f, err := s.DeleteFolder(c.Params(":uid"))
if err != nil {
return toFolderError(err)
}
return JSON(200, util.DynMap{
"title": f.Title,
"message": fmt.Sprintf("Folder %s deleted", f.Title),
})
}
func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder {
canEdit, _ := g.CanEdit()
canSave, _ := g.CanSave()
canAdmin, _ := g.CanAdmin()
// Finding creator and last updater of the folder
updater, creator := "Anonymous", "Anonymous"
if folder.CreatedBy > 0 {
creator = getUserLogin(folder.CreatedBy)
}
if folder.UpdatedBy > 0 {
updater = getUserLogin(folder.UpdatedBy)
}
return dtos.Folder{
Id: folder.Id,
Uid: folder.Uid,
Title: folder.Title,
Url: folder.Url,
HasAcl: folder.HasAcl,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CreatedBy: creator,
Created: folder.Created,
UpdatedBy: updater,
Updated: folder.Updated,
Version: folder.Version,
}
}
func toFolderError(err error) Response {
if err == m.ErrFolderTitleEmpty ||
err == m.ErrFolderSameNameExists ||
err == m.ErrFolderWithSameUIDExists ||
err == m.ErrDashboardTypeMismatch ||
err == m.ErrDashboardInvalidUid ||
err == m.ErrDashboardUidToLong {
return Error(400, err.Error(), nil)
}
if err == m.ErrFolderAccessDenied {
return Error(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 Error(500, "Folder API error", err)
}
+107
View File
@@ -0,0 +1,107 @@
package api
import (
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
)
func GetFolderPermissionList(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUID(c.Params(":uid"))
if err != nil {
return toFolderError(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return toFolderError(m.ErrFolderAccessDenied)
}
acl, err := g.GetAcl()
if err != nil {
return Error(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 Error(400, err.Error(), err)
}
return Error(500, "Error while checking folder permissions", err)
}
return Error(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 Error(409, err.Error(), err)
}
return Error(500, "Failed to create permission", err)
}
return Success("Folder permissions updated")
}
+241
View File
@@ -0,0 +1,241 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
. "github.com/smartystreets/goconvey/convey"
)
func TestFolderPermissionApiEndpoint(t *testing.T) {
Convey("Folder permissions test", t, func() {
Convey("Given folder not exists", func() {
mock := &fakeFolderService{
GetFolderByUIDError: m.ErrFolderNotFound,
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 404)
})
Reset(func() {
dashboards.NewFolderService = origNewFolderService
})
})
Convey("Given user has no admin permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
mock := &fakeFolderService{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_EDITOR, func(sc *scenarioContext) {
callGetFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
Convey("Given user has admin permissions and permissions to update", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: true,
GetAclValue: []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
{OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
},
})
mock := &fakeFolderService{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", m.ROLE_ADMIN, func(sc *scenarioContext) {
callGetFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 5)
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
})
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
Convey("When trying to update permissions with duplicate permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
})
mock := &fakeFolderService{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
Convey("When trying to override inherited permissions with lower presedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
CheckPermissionBeforeUpdateValue: false,
CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
)
mock := &fakeFolderService{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
},
}
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
cmd := dtos.UpdateDashboardAclCommand{
Items: []dtos.DashboardAclUpdateItem{
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
},
}
updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) {
callUpdateFolderPermissions(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
Reset(func() {
guardian.New = origNewGuardian
dashboards.NewFolderService = origNewFolderService
})
})
})
}
func callGetFolderPermissions(sc *scenarioContext) {
sc.handlerFunc = GetFolderPermissionList
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func callUpdateFolderPermissions(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.UpdateDashboardAclCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func updateFolderPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.OrgId = TestOrgID
sc.context.UserId = TestUserID
return UpdateFolderPermissions(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
+251
View File
@@ -0,0 +1,251 @@
package api
import (
"encoding/json"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
. "github.com/smartystreets/goconvey/convey"
)
func TestFoldersApiEndpoint(t *testing.T) {
Convey("Create/update folder response tests", t, func() {
Convey("Given a correct request for creating a folder", func() {
cmd := m.CreateFolderCommand{
Uid: "uid",
Title: "Folder",
}
mock := &fakeFolderService{
CreateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder"},
}
createFolderScenario("When calling POST on", "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
Convey("It should return correct response data", func() {
folder := dtos.Folder{}
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
So(err, ShouldBeNil)
So(folder.Id, ShouldEqual, 1)
So(folder.Uid, ShouldEqual, "uid")
So(folder.Title, ShouldEqual, "Folder")
})
})
})
Convey("Given incorrect requests for creating a folder", func() {
testCases := []struct {
Error error
ExpectedStatusCode int
}{
{Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400},
{Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
{Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400},
{Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403},
{Error: m.ErrFolderNotFound, ExpectedStatusCode: 404},
{Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
{Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500},
}
cmd := m.CreateFolderCommand{
Uid: "uid",
Title: "Folder",
}
for _, tc := range testCases {
mock := &fakeFolderService{
CreateFolderError: tc.Error,
}
createFolderScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()), "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
if sc.resp.Code != tc.ExpectedStatusCode {
t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code)
}
})
}
})
Convey("Given a correct request for updating a folder", func() {
cmd := m.UpdateFolderCommand{
Title: "Folder upd",
}
mock := &fakeFolderService{
UpdateFolderResult: &m.Folder{Id: 1, Uid: "uid", Title: "Folder upd"},
}
updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
callUpdateFolder(sc)
Convey("It should return correct response data", func() {
folder := dtos.Folder{}
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
So(err, ShouldBeNil)
So(folder.Id, ShouldEqual, 1)
So(folder.Uid, ShouldEqual, "uid")
So(folder.Title, ShouldEqual, "Folder upd")
})
})
})
Convey("Given incorrect requests for updating a folder", func() {
testCases := []struct {
Error error
ExpectedStatusCode int
}{
{Error: m.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400},
{Error: m.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
{Error: m.ErrFolderSameNameExists, ExpectedStatusCode: 400},
{Error: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{Error: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{Error: m.ErrFolderAccessDenied, ExpectedStatusCode: 403},
{Error: m.ErrFolderNotFound, ExpectedStatusCode: 404},
{Error: m.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
{Error: m.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500},
}
cmd := m.UpdateFolderCommand{
Title: "Folder upd",
}
for _, tc := range testCases {
mock := &fakeFolderService{
UpdateFolderError: tc.Error,
}
updateFolderScenario(fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()), "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
callUpdateFolder(sc)
if sc.resp.Code != tc.ExpectedStatusCode {
t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code)
}
})
}
})
})
}
func callGetFolderByUID(sc *scenarioContext) {
sc.handlerFunc = GetFolderByUID
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func callDeleteFolder(sc *scenarioContext) {
sc.handlerFunc = DeleteFolder
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func callCreateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func createFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.CreateFolderCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
return CreateFolder(c, cmd)
})
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
sc.m.Post(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewFolderService = origNewFolderService
}()
fn(sc)
})
}
func callUpdateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
}
func updateFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd m.UpdateFolderCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
return UpdateFolder(c, cmd)
})
origNewFolderService := dashboards.NewFolderService
mockFolderService(mock)
sc.m.Put(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewFolderService = origNewFolderService
}()
fn(sc)
})
}
type fakeFolderService struct {
GetFoldersResult []*m.Folder
GetFoldersError error
GetFolderByUIDResult *m.Folder
GetFolderByUIDError error
GetFolderByIDResult *m.Folder
GetFolderByIDError error
CreateFolderResult *m.Folder
CreateFolderError error
UpdateFolderResult *m.Folder
UpdateFolderError error
DeleteFolderResult *m.Folder
DeleteFolderError error
DeletedFolderUids []string
}
func (s *fakeFolderService) GetFolders(limit int) ([]*m.Folder, error) {
return s.GetFoldersResult, s.GetFoldersError
}
func (s *fakeFolderService) GetFolderByID(id int64) (*m.Folder, error) {
return s.GetFolderByIDResult, s.GetFolderByIDError
}
func (s *fakeFolderService) GetFolderByUID(uid string) (*m.Folder, error) {
return s.GetFolderByUIDResult, s.GetFolderByUIDError
}
func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
cmd.Result = s.CreateFolderResult
return s.CreateFolderError
}
func (s *fakeFolderService) UpdateFolder(existingUID string, cmd *m.UpdateFolderCommand) error {
cmd.Result = s.UpdateFolderResult
return s.UpdateFolderError
}
func (s *fakeFolderService) DeleteFolder(uid string) (*m.Folder, error) {
s.DeletedFolderUids = append(s.DeletedFolderUids, uid)
return s.DeleteFolderResult, s.DeleteFolderError
}
func mockFolderService(mock *fakeFolderService) {
dashboards.NewFolderService = func(orgId int64, user *m.SignedInUser) dashboards.FolderService {
return mock
}
}
+31 -27
View File
@@ -5,14 +5,13 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, error) {
func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
orgDataSources := make([]*m.DataSource, 0)
if c.OrgId != 0 {
@@ -42,6 +41,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
}
var dsMap = map[string]interface{}{
"id": ds.Id,
"type": ds.Type,
"name": ds.Name,
"url": url,
@@ -61,6 +61,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
if ds.JsonData != nil {
dsMap["jsonData"] = ds.JsonData
} else {
dsMap["jsonData"] = make(map[string]string)
}
if ds.Access == m.DS_ACCESS_DIRECT {
@@ -101,18 +103,15 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
datasources[ds.Name] = dsMap
}
// add grafana backend data source
grafanaDatasourceMeta, _ := plugins.DataSources["grafana"]
datasources["-- Grafana --"] = map[string]interface{}{
"type": "grafana",
"name": "-- Grafana --",
"meta": grafanaDatasourceMeta,
}
// add mixed backend data source
datasources["-- Mixed --"] = map[string]interface{}{
"type": "mixed",
"meta": plugins.DataSources["mixed"],
// add datasources that are built in (meaning they are not added via data sources page, nor have any entry in datasource table)
for _, ds := range plugins.DataSources {
if ds.BuiltIn {
datasources[ds.Name] = map[string]interface{}{
"type": ds.Type,
"name": ds.Name,
"meta": plugins.DataSources[ds.Id],
}
}
}
if defaultDatasource == "" {
@@ -133,16 +132,19 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
}
jsonObj := map[string]interface{}{
"defaultDatasource": defaultDatasource,
"datasources": datasources,
"panels": panels,
"appSubUrl": setting.AppSubUrl,
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm,
"defaultDatasource": defaultDatasource,
"datasources": datasources,
"panels": panels,
"appSubUrl": setting.AppSubUrl,
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm,
"externalUserMngInfo": setting.ExternalUserMngInfo,
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,
@@ -167,15 +169,17 @@ func getPanelSort(id string) int {
sort = 3
case "text":
sort = 4
case "alertlist":
case "heatmap":
sort = 5
case "dashlist":
case "alertlist":
sort = 6
case "dashlist":
sort = 7
}
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)
@@ -1,30 +1,29 @@
package api
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"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"
)
var gNetProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
Proxy: http.ProxyFromEnvironment,
var grafanaComProxyTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
url, _ := url.Parse(setting.GrafanaNetUrl)
url, _ := url.Parse(setting.GrafanaComUrl)
director := func(req *http.Request) {
req.URL.Scheme = url.Scheme
@@ -42,10 +41,10 @@ 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 = gNetProxyTransport
proxy.Transport = grafanaComProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie")
}
+88 -24
View File
@@ -5,37 +5,48 @@ import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"os"
"path"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
gocache "github.com/patrickmn/go-cache"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/live"
httpstatic "github.com/grafana/grafana/pkg/api/static"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
type HttpServer struct {
type HTTPServer struct {
log log.Logger
macaron *macaron.Macaron
context context.Context
streamManager *live.StreamManager
cache *gocache.Cache
httpSrv *http.Server
}
func NewHttpServer() *HttpServer {
return &HttpServer{
log: log.New("http.server"),
func NewHTTPServer() *HTTPServer {
return &HTTPServer{
log: log.New("http.server"),
cache: gocache.New(5*time.Minute, 10*time.Minute),
}
}
func (hs *HttpServer) Start(ctx context.Context) error {
func (hs *HTTPServer) Start(ctx context.Context) error {
var err error
hs.context = ctx
@@ -46,7 +57,7 @@ func (hs *HttpServer) Start(ctx context.Context) error {
hs.streamManager.Run(ctx)
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
switch setting.Protocol {
@@ -57,11 +68,26 @@ func (hs *HttpServer) Start(ctx context.Context) error {
return nil
}
case setting.HTTPS:
err = hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
err = hs.listenAndServeTLS(setting.CertFile, setting.KeyFile)
if err == http.ErrServerClosed {
hs.log.Debug("server was shutdown gracefully")
return nil
}
case setting.SOCKET:
ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: setting.SocketPath, Net: "unix"})
if err != nil {
hs.log.Debug("server was shutdown gracefully")
return nil
}
// Make socket writable by group
os.Chmod(setting.SocketPath, 0660)
err = hs.httpSrv.Serve(ln)
if err != nil {
hs.log.Debug("server was shutdown gracefully")
return nil
}
default:
hs.log.Error("Invalid protocol", "protocol", setting.Protocol)
err = errors.New("Invalid Protocol")
@@ -70,13 +96,13 @@ func (hs *HttpServer) Start(ctx context.Context) error {
return err
}
func (hs *HttpServer) Shutdown(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
}
func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) error {
func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
if certfile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
}
@@ -111,45 +137,48 @@ func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) er
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
},
}
srv := &http.Server{
Addr: listenAddr,
Handler: hs.macaron,
TLSConfig: tlsCfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
return srv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
hs.httpSrv.TLSConfig = tlsCfg
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
}
func (hs *HttpServer) newMacaron() *macaron.Macaron {
func (hs *HTTPServer) newMacaron() *macaron.Macaron {
macaron.Env = setting.Env
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)
logger.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
hs.mapStatic(m, route.Directory, "", pluginRoute)
}
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,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
m.Use(hs.healthHandler)
m.Use(hs.metricsEndpoint)
m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(&setting.SessionOptions))
m.Use(middleware.RequestMetrics())
m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
m.Use(middleware.OrgRedirect())
// needs to be after context handler
@@ -157,10 +186,45 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
m.Use(middleware.ValidateHostHeader(setting.Domain))
}
m.Use(middleware.AddDefaultResponseHeaders())
return m
}
func (hs *HttpServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" {
return
}
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
ServeHTTP(ctx.Resp, ctx.Req.Request)
}
func (hs *HTTPServer) healthHandler(ctx *macaron.Context) {
notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
if notHeadOrGet || ctx.Req.URL.Path != "/api/health" {
return
}
data := simplejson.New()
data.Set("database", "ok")
data.Set("version", setting.BuildVersion)
data.Set("commit", setting.BuildCommit)
if err := bus.Dispatch(&models.GetDBHealthQuery{}); err != nil {
data.Set("database", "failing")
ctx.Resp.Header().Set("Content-Type", "application/json; charset=UTF-8")
ctx.Resp.WriteHeader(503)
} else {
ctx.Resp.Header().Set("Content-Type", "application/json; charset=UTF-8")
ctx.Resp.WriteHeader(200)
}
dataBytes, _ := data.EncodePretty()
ctx.Resp.Write(dataBytes)
}
func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
headers := func(c *macaron.Context) {
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
}
+167 -62
View File
@@ -6,13 +6,12 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
settings, err := getFrontendSettingsMap(c)
if err != nil {
return nil, err
@@ -33,13 +32,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
locale = parts[0]
}
appUrl := setting.AppUrl
appSubUrl := setting.AppSubUrl
appURL := setting.AppUrl
appSubURL := setting.AppSubUrl
// special case when doing localhost call from phantomjs
if c.IsRenderCall {
appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
appSubUrl = ""
appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
appSubURL = ""
settings["appSubUrl"] = ""
}
@@ -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,8 +61,9 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
HelpFlags1: c.HelpFlags1,
},
Settings: settings,
AppUrl: appUrl,
AppSubUrl: appSubUrl,
Theme: prefs.Theme,
AppUrl: appURL,
AppSubUrl: appSubURL,
GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleTagManagerId: setting.GoogleTagManagerId,
BuildVersion: setting.BuildVersion,
@@ -72,62 +73,87 @@ 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 {
data.User.Name = data.User.Login
}
themeUrlParam := c.Query("theme")
if themeUrlParam == "light" {
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,51 +195,129 @@ 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) {
if data, err := setIndexViewData(c); err != nil {
func Index(c *m.ReqContext) {
data, err := setIndexViewData(c)
if err != nil {
c.Handle(500, "Failed to get settings", err)
return
} else {
c.HTML(200, "index", data)
}
c.HTML(200, "index", data)
}
func NotFoundHandler(c *middleware.Context) {
func NotFoundHandler(c *m.ReqContext) {
if c.IsApiRequest() {
c.JsonApiErr(404, "Not found", nil)
return
}
if data, err := setIndexViewData(c); err != nil {
data, err := setIndexViewData(c)
if err != nil {
c.Handle(500, "Failed to get settings", err)
return
} else {
c.HTML(404, "index", data)
}
c.HTML(404, "index", data)
}
+30 -23
View File
@@ -8,17 +8,16 @@ 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"
"github.com/grafana/grafana/pkg/util"
)
const (
VIEW_INDEX = "index"
ViewIndex = "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)
@@ -35,8 +34,13 @@ func LoginView(c *middleware.Context) {
viewData.Settings["loginHint"] = setting.LoginHint
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
if loginError, ok := c.Session.Get("loginError").(string); ok {
c.Session.Delete("loginError")
viewData.Settings["loginError"] = loginError
}
if !tryLoginUsingRememberCookie(c) {
c.HTML(200, VIEW_INDEX, viewData)
c.HTML(200, ViewIndex, viewData)
return
}
@@ -49,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 {
@@ -74,8 +78,7 @@ func tryLoginUsingRememberCookie(c *middleware.Context) bool {
user := userQuery.Result
// validate remember me cookie
if val, _ := c.GetSuperSecureCookie(
util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName); val != user.Login {
if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login {
return false
}
@@ -84,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
@@ -93,22 +96,23 @@ 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)
return Error(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 {
return ApiError(401, "Invalid username or password", err)
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
return Error(401, "Invalid username or password", err)
}
return ApiError(500, "Error while trying to authenticate user", err)
return Error(500, "Error while trying to authenticate user", err)
}
user := authQuery.User
@@ -124,28 +128,31 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response {
c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
}
metrics.M_Api_Login_Post.Inc(1)
metrics.M_Api_Login_Post.Inc()
return Json(200, result)
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")
}
c.Resp.Header().Del("Set-Cookie")
days := 86400 * setting.LogInRememberDays
if days > 0 {
c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/")
c.SetSuperSecureCookie(util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/")
c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/")
}
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")
}
+73 -45
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
@@ -8,62 +9,75 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"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"
)
var (
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
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")
)
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
}
error := ctx.Query("error")
if error != "" {
errorParam := ctx.Query("error")
if errorParam != "" {
errorDesc := ctx.Query("error_description")
ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc)
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003")
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
return
}
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 {
ctx.Redirect(connect.AuthCodeURL(state, oauth2.SetParam("hd", setting.OAuthService.OAuthInfos[name].HostedDomain), oauth2.AccessTypeOnline))
ctx.Redirect(connect.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", setting.OAuthService.OAuthInfos[name].HostedDomain), oauth2.AccessTypeOnline))
}
return
}
// verify state string
savedState := 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
}
queryState := ctx.Query("state")
if savedState != queryState {
ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
@@ -71,36 +85,41 @@ func OAuthLogin(ctx *middleware.Context) {
}
// handle call back
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: setting.OAuthService.OAuthInfos[name].TlsSkipVerify,
},
}
oauthClient := &http.Client{
Transport: tr,
}
// initialize oauth2 context
oauthCtx := oauth2.NoContext
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" {
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(err)
ctx.Logger.Error("Failed to setup TlsClientCert", "oauth", name, "error", err)
ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCert)", nil)
return
}
// Load CA cert
tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
}
if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
if err != nil {
log.Fatal(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)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
},
}
sslcli := &http.Client{Transport: tr}
oauthCtx = context.Background()
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
tr.TLSClientConfig.RootCAs = caCertPool
}
oauthCtx := context.WithValue(context.Background(), oauth2.HTTPClient, oauthClient)
// get token from provider
token, err := connect.Exchange(oauthCtx, code)
if err != nil {
@@ -110,30 +129,33 @@ 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 err == social.ErrMissingTeamMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
} else if err == social.ErrMissingOrganizationMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001")
if sErr, ok := err.(*social.Error); ok {
redirectWithError(ctx, sErr)
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
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 == "" {
redirectWithError(ctx, ErrNoEmail)
return
}
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
redirectWithError(ctx, ErrEmailNotAllowed)
return
}
@@ -143,16 +165,16 @@ func OAuthLogin(ctx *middleware.Context) {
// create account if missing
if err == m.ErrUserNotFound {
if !connect.IsSignupAllowed() {
ctx.Redirect(setting.AppSubUrl + "/login")
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
}
if limitReached {
ctx.Redirect(setting.AppSubUrl + "/login")
redirectWithError(ctx, ErrUsersQuotaReached)
return
}
cmd := m.CreateUserCommand{
@@ -176,7 +198,7 @@ func OAuthLogin(ctx *middleware.Context) {
// login
loginUserWithUser(userQuery.Result, ctx)
metrics.M_Api_Login_OAuth.Inc(1)
metrics.M_Api_Login_OAuth.Inc()
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
@@ -186,3 +208,9 @@ func OAuthLogin(ctx *middleware.Context) {
ctx.Redirect(setting.AppSubUrl + "/")
}
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")
}
+72 -70
View File
@@ -2,23 +2,35 @@ package api
import (
"context"
"encoding/json"
"net/http"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"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/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)
request := &tsdb.Request{TimeRange: timeRange}
if len(reqDto.Queries) == 0 {
return Error(400, "No queries found in query", nil)
}
dsID, err := reqDto.Queries[0].Get("datasourceId").Int64()
if err != nil {
return Error(400, "Query missing datasourceId", nil)
}
dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId}
if err := bus.Dispatch(&dsQuery); err != nil {
return Error(500, "failed to fetch data source", err)
}
request := &tsdb.TsdbQuery{TimeRange: timeRange}
for _, query := range reqDto.Queries {
request.Queries = append(request.Queries, &tsdb.Query{
@@ -26,23 +38,29 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
IntervalMs: query.Get("intervalMs").MustInt64(1000),
Model: query,
DataSource: &models.DataSource{
Name: "Grafana TestDataDB",
Type: "grafana-testdata-datasource",
},
DataSource: dsQuery.Result,
})
}
resp, err := tsdb.HandleRequest(context.Background(), request)
resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request)
if err != nil {
return ApiError(500, "Metric request error", err)
return Error(500, "Metric request error", err)
}
return Json(200, &resp)
statusCode := 200
for _, res := range resp.Results {
if res.Error != nil {
res.ErrorString = res.Error.Error()
resp.Message = res.ErrorString
statusCode = 500
}
}
return JSON(statusCode, &resp)
}
// 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 {
@@ -54,63 +72,47 @@ func GetTestDataScenarios(c *middleware.Context) Response {
})
}
return Json(200, &result)
}
func GetInternalMetrics(c *middleware.Context) Response {
if metrics.UseNilMetrics {
return Json(200, util.DynMap{"message": "Metrics disabled"})
}
snapshots := metrics.MetricStats.GetSnapshots()
resp := make(map[string]interface{})
for _, m := range snapshots {
metricName := m.Name() + m.StringifyTags()
switch metric := m.(type) {
case metrics.Gauge:
resp[metricName] = map[string]interface{}{
"value": metric.Value(),
}
case metrics.Counter:
resp[metricName] = map[string]interface{}{
"count": metric.Count(),
}
case metrics.Timer:
percentiles := metric.Percentiles([]float64{0.25, 0.75, 0.90, 0.99})
resp[metricName] = map[string]interface{}{
"count": metric.Count(),
"min": metric.Min(),
"max": metric.Max(),
"mean": metric.Mean(),
"std": metric.StdDev(),
"p25": percentiles[0],
"p75": percentiles[1],
"p90": percentiles[2],
"p99": percentiles[3],
}
}
}
var b []byte
var err error
if b, err = json.MarshalIndent(resp, "", " "); err != nil {
return ApiError(500, "body json marshal", err)
}
return &NormalResponse{
body: b,
status: 200,
header: http.Header{
"Content-Type": []string{"application/json"},
},
}
return JSON(200, &result)
}
// 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])
return JSON(200, array[20])
}
// GET /api/tsdb/testdata/gensql
func GenerateSQLTestData(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil {
return Error(500, "Failed to insert test data", err)
}
return JSON(200, &util.DynMap{"message": "OK"})
}
// GET /api/tsdb/testdata/random-walk
func GetTestDataRandomWalk(c *m.ReqContext) Response {
from := c.Query("from")
to := c.Query("to")
intervalMs := c.QueryInt64("intervalMs")
timeRange := tsdb.NewTimeRange(from, to)
request := &tsdb.TsdbQuery{TimeRange: timeRange}
dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"}
request.Queries = append(request.Queries, &tsdb.Query{
RefId: "A",
IntervalMs: intervalMs,
Model: simplejson.NewFromAny(&util.DynMap{
"scenario": "random_walk",
}),
DataSource: dsInfo,
})
resp, err := tsdb.HandleRequest(context.Background(), dsInfo, request)
if err != nil {
return Error(500, "Metric request error", err)
}
return JSON(200, &resp)
}
+37 -38
View File
@@ -4,31 +4,30 @@ 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 {
return ApiError(404, "Organization not found", err)
return Error(404, "Organization not found", err)
}
return ApiError(500, "Failed to get organization", err)
return Error(500, "Failed to get organization", err)
}
org := query.Result
result := m.OrgDetailsDTO{
@@ -44,18 +43,18 @@ func GetOrgByName(c *middleware.Context) Response {
},
}
return Json(200, &result)
return JSON(200, &result)
}
func getOrgHelper(orgId int64) Response {
query := m.GetOrgByIdQuery{Id: orgId}
func getOrgHelper(orgID int64) Response {
query := m.GetOrgByIdQuery{Id: orgID}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrOrgNotFound {
return ApiError(404, "Organization not found", err)
return Error(404, "Organization not found", err)
}
return ApiError(500, "Failed to get organization", err)
return Error(500, "Failed to get organization", err)
}
org := query.Result
@@ -72,66 +71,66 @@ func getOrgHelper(orgId int64) Response {
},
}
return Json(200, &result)
return JSON(200, &result)
}
// 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)
return Error(403, "Access denied", nil)
}
cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrOrgNameTaken {
return ApiError(409, "Organization name taken", err)
return Error(409, "Organization name taken", err)
}
return ApiError(500, "Failed to create organization", err)
return Error(500, "Failed to create organization", err)
}
metrics.M_Api_Org_Create.Inc(1)
metrics.M_Api_Org_Create.Inc()
return Json(200, &util.DynMap{
return JSON(200, &util.DynMap{
"orgId": cmd.Result.Id,
"message": "Organization created",
})
}
// 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"))
}
func updateOrgHelper(form dtos.UpdateOrgForm, orgId int64) Response {
cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgId}
func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) Response {
cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgID}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrOrgNameTaken {
return ApiError(400, "Organization name taken", err)
return Error(400, "Organization name taken", err)
}
return ApiError(500, "Failed to update organization", err)
return Error(500, "Failed to update organization", err)
}
return ApiSuccess("Organization updated")
return Success("Organization updated")
}
// 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"))
}
func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Response {
func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgID int64) Response {
cmd := m.UpdateOrgAddressCommand{
OrgId: orgId,
OrgId: orgID,
Address: m.Address{
Address1: form.Address1,
Address2: form.Address2,
@@ -143,24 +142,24 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Respons
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update org address", err)
return Error(500, "Failed to update org address", err)
}
return ApiSuccess("Address updated")
return Success("Address updated")
}
// 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)
return Error(404, "Failed to delete organization. ID not found", nil)
}
return ApiError(500, "Failed to update organization", err)
return Error(500, "Failed to update organization", err)
}
return ApiSuccess("Organization deleted")
return Success("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"),
@@ -169,8 +168,8 @@ func SearchOrgs(c *middleware.Context) Response {
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search orgs", err)
return Error(500, "Failed to search orgs", err)
}
return Json(200, query.Result)
return JSON(200, query.Result)
}
+55 -52
View File
@@ -7,40 +7,39 @@ 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 {
return ApiError(500, "Failed to get invites from db", err)
return Error(500, "Failed to get invites from db", err)
}
for _, invite := range query.Result {
invite.Url = setting.ToAbsUrl("invite/" + invite.Code)
}
return Json(200, query.Result)
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)
return Error(400, "Invalid role specified", nil)
}
// first try get existing user
userQuery := m.GetUserByLoginQuery{LoginOrEmail: inviteDto.LoginOrEmail}
if err := bus.Dispatch(&userQuery); err != nil {
if err != m.ErrUserNotFound {
return ApiError(500, "Failed to query db for existing user check", err)
return Error(500, "Failed to query db for existing user check", err)
}
if setting.DisableLoginForm {
return ApiError(401, "User could not be found", nil)
return Error(401, "User could not be found", nil)
}
} else {
return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
@@ -57,11 +56,11 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
cmd.RemoteAddr = c.Req.RemoteAddr
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to save invite to database", err)
return Error(500, "Failed to save invite to database", err)
}
// 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",
@@ -75,66 +74,70 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
}
if err := bus.Dispatch(&emailCmd); err != nil {
return ApiError(500, "Failed to send email invite", err)
return Error(500, "Failed to send email invite", err)
}
return ApiSuccess(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
if err := bus.Dispatch(&emailSentCmd); err != nil {
return Error(500, "Failed to update invite with email sent info", err)
}
return Success(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
}
return ApiSuccess(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail))
return Success(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 {
if err == m.ErrOrgUserAlreadyAdded {
return ApiError(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err)
return Error(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err)
}
return ApiError(500, "Error while trying to create org user", err)
} else {
if !inviteDto.SkipEmails && util.IsEmail(user.Email) {
emailCmd := m.SendEmailCommand{
To: []string{user.Email},
Template: "invited_to_org.html",
Data: map[string]interface{}{
"Name": user.NameOrFallback(),
"OrgName": c.OrgName,
"InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login),
},
}
if err := bus.Dispatch(&emailCmd); err != nil {
return ApiError(500, "Failed to send email invited_to_org", err)
}
}
return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
return Error(500, "Error while trying to create org user", err)
}
if inviteDto.SendEmail && util.IsEmail(user.Email) {
emailCmd := m.SendEmailCommand{
To: []string{user.Email},
Template: "invited_to_org.html",
Data: map[string]interface{}{
"Name": user.NameOrFallback(),
"OrgName": c.OrgName,
"InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login),
},
}
if err := bus.Dispatch(&emailCmd); err != nil {
return Error(500, "Failed to send email invited_to_org", err)
}
}
return Success(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
}
func RevokeInvite(c *middleware.Context) Response {
func RevokeInvite(c *m.ReqContext) Response {
if ok, rsp := updateTempUserStatus(c.Params(":code"), m.TmpUserRevoked); !ok {
return rsp
}
return ApiSuccess("Invite revoked")
return Success("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 {
if err == m.ErrTempUserNotFound {
return ApiError(404, "Invite not found", nil)
return Error(404, "Invite not found", nil)
}
return ApiError(500, "Failed to get invite", err)
return Error(500, "Failed to get invite", err)
}
invite := query.Result
return Json(200, dtos.InviteInfo{
return JSON(200, dtos.InviteInfo{
Email: invite.Email,
Name: invite.Name,
Username: invite.Email,
@@ -142,19 +145,19 @@ 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 {
if err == m.ErrTempUserNotFound {
return ApiError(404, "Invite not found", nil)
return Error(404, "Invite not found", nil)
}
return ApiError(500, "Failed to get invite", err)
return Error(500, "Failed to get invite", err)
}
invite := query.Result
if invite.Status != m.TmpUserInvitePending {
return ApiError(412, fmt.Sprintf("Invite cannot be used in status %s", invite.Status), nil)
return Error(412, fmt.Sprintf("Invite cannot be used in status %s", invite.Status), nil)
}
cmd := m.CreateUserCommand{
@@ -166,7 +169,7 @@ func CompleteInvite(c *middleware.Context, completeInvite dtos.CompleteInviteFor
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "failed to create user", err)
return Error(500, "failed to create user", err)
}
user := &cmd.Result
@@ -182,17 +185,17 @@ func CompleteInvite(c *middleware.Context, completeInvite dtos.CompleteInviteFor
loginUserWithUser(user, c)
metrics.M_Api_User_SignUpCompleted.Inc(1)
metrics.M_Api_User_SignUpInvite.Inc(1)
metrics.M_Api_User_SignUpCompleted.Inc()
metrics.M_Api_User_SignUpInvite.Inc()
return ApiSuccess("User created and logged in")
return Success("User created and logged in")
}
func updateTempUserStatus(code string, status m.TempUserStatus) (bool, Response) {
// update temp user status
updateTmpUserCmd := m.UpdateTempUserStatusCommand{Code: code, Status: status}
if err := bus.Dispatch(&updateTmpUserCmd); err != nil {
return false, ApiError(500, "Failed to update invite status", err)
return false, Error(500, "Failed to update invite status", err)
}
return true, nil
@@ -203,7 +206,7 @@ func applyUserInvite(user *m.User, invite *m.TempUserDTO, setActive bool) (bool,
addOrgUserCmd := m.AddOrgUserCommand{OrgId: invite.OrgId, UserId: user.Id, Role: invite.Role}
if err := bus.Dispatch(&addOrgUserCmd); err != nil {
if err != m.ErrOrgUserAlreadyAdded {
return false, ApiError(500, "Error while trying to create org user", err)
return false, Error(500, "Error while trying to create org user", err)
}
}
@@ -215,7 +218,7 @@ func applyUserInvite(user *m.User, invite *m.TempUserDTO, setActive bool) (bool,
if setActive {
// set org to active
if err := bus.Dispatch(&m.SetUsingOrgCommand{OrgId: invite.OrgId, UserId: user.Id}); err != nil {
return false, ApiError(500, "Failed to set org as active", err)
return false, Error(500, "Failed to set org as active", err)
}
}
+44 -40
View File
@@ -1,81 +1,85 @@
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)
}
func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
if !cmd.Role.IsValid() {
return ApiError(400, "Invalid role specified", nil)
return Error(400, "Invalid role specified", nil)
}
userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
err := bus.Dispatch(&userQuery)
if err != nil {
return ApiError(404, "User not found", nil)
return Error(404, "User not found", nil)
}
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 {
if err == m.ErrOrgUserAlreadyAdded {
return ApiError(409, "User is already member of this organization", nil)
return Error(409, "User is already member of this organization", nil)
}
return ApiError(500, "Could not add user to organization", err)
return Error(500, "Could not add user to organization", err)
}
return ApiSuccess("User added to organization")
return Success("User added to organization")
}
// 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}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get account user", err)
func getOrgUsersHelper(orgID int64, query string, limit int) Response {
q := m.GetOrgUsersQuery{
OrgId: orgID,
Query: query,
Limit: limit,
}
return Json(200, query.Result)
if err := bus.Dispatch(&q); err != nil {
return Error(500, "Failed to get account user", err)
}
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)
@@ -83,41 +87,41 @@ func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
if !cmd.Role.IsValid() {
return ApiError(400, "Invalid role specified", nil)
return Error(400, "Invalid role specified", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
return ApiError(400, "Cannot change role so that there is no organization admin left", nil)
return Error(400, "Cannot change role so that there is no organization admin left", nil)
}
return ApiError(500, "Failed update org user", err)
return Error(500, "Failed update org user", err)
}
return ApiSuccess("Organization user updated")
return Success("Organization user updated")
}
// DELETE /api/org/users/:userId
func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response {
userId := c.ParamsInt64(":userId")
return removeOrgUserHelper(c.OrgId, userId)
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 {
userId := c.ParamsInt64(":userId")
orgId := c.ParamsInt64(":orgId")
return removeOrgUserHelper(orgId, userId)
func RemoveOrgUser(c *m.ReqContext) Response {
userID := c.ParamsInt64(":userId")
orgID := c.ParamsInt64(":orgId")
return removeOrgUserHelper(orgID, userID)
}
func removeOrgUserHelper(orgId int64, userId int64) Response {
cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId}
func removeOrgUserHelper(orgID int64, userID int64) Response {
cmd := m.RemoveOrgUserCommand{OrgId: orgID, UserId: userID}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
return ApiError(400, "Cannot remove last organization admin", nil)
return Error(400, "Cannot remove last organization admin", nil)
}
return ApiError(500, "Failed to remove user from organization", err)
return Error(500, "Failed to remove user from organization", err)
}
return ApiSuccess("User removed from organization")
return Success("User removed from organization")
}
+11 -11
View File
@@ -3,38 +3,38 @@ 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 {
return ApiError(404, "User does not exist", err)
c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail)
return Error(200, "Email sent", err)
}
emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result}
if err := bus.Dispatch(&emailCmd); err != nil {
return ApiError(500, "Failed to send email", err)
return Error(500, "Failed to send email", err)
}
return ApiSuccess("Email sent")
return Success("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 {
if err == m.ErrInvalidEmailCode {
return ApiError(400, "Invalid or expired reset password code", nil)
return Error(400, "Invalid or expired reset password code", nil)
}
return ApiError(500, "Unknown error validating email code", err)
return Error(500, "Unknown error validating email code", err)
}
if form.NewPassword != form.ConfirmPassword {
return ApiError(400, "Passwords do not match", nil)
return Error(400, "Passwords do not match", nil)
}
cmd := m.ChangeUserPasswordCommand{}
@@ -42,8 +42,8 @@ func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Respo
cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt)
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to change user password", err)
return Error(500, "Failed to change user password", err)
}
return ApiSuccess("User password changed")
return Success("User password changed")
}
+25 -26
View File
@@ -3,11 +3,10 @@ package api
import (
"github.com/grafana/grafana/pkg/bus"
_ "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
func ValidateOrgPlaylist(c *middleware.Context) {
func ValidateOrgPlaylist(c *m.ReqContext) {
id := c.ParamsInt64(":id")
query := m.GetPlaylistByIdQuery{Id: id}
err := bus.Dispatch(&query)
@@ -40,7 +39,7 @@ func ValidateOrgPlaylist(c *middleware.Context) {
}
}
func SearchPlaylists(c *middleware.Context) Response {
func SearchPlaylists(c *m.ReqContext) Response {
query := c.Query("query")
limit := c.QueryInt("limit")
@@ -56,18 +55,18 @@ func SearchPlaylists(c *middleware.Context) Response {
err := bus.Dispatch(&searchQuery)
if err != nil {
return ApiError(500, "Search failed", err)
return Error(500, "Search failed", err)
}
return Json(200, searchQuery.Result)
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}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Playlist not found", err)
return Error(500, "Playlist not found", err)
}
playlistDTOs, _ := LoadPlaylistItemDTOs(id)
@@ -80,7 +79,7 @@ func GetPlaylist(c *middleware.Context) Response {
Items: playlistDTOs,
}
return Json(200, dto)
return JSON(200, dto)
}
func LoadPlaylistItemDTOs(id int64) ([]m.PlaylistItemDTO, error) {
@@ -115,62 +114,62 @@ 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)
if err != nil {
return ApiError(500, "Could not load playlist items", err)
return Error(500, "Could not load playlist items", err)
}
return Json(200, playlistDTOs)
return JSON(200, playlistDTOs)
}
func GetPlaylistDashboards(c *middleware.Context) Response {
playlistId := c.ParamsInt64(":id")
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)
return Error(500, "Could not load dashboards", err)
}
return Json(200, playlists)
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}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete playlist", err)
return Error(500, "Failed to delete playlist", err)
}
return Json(200, "")
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 {
return ApiError(500, "Failed to create playlist", err)
return Error(500, "Failed to create playlist", err)
}
return Json(200, cmd.Result)
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 {
return ApiError(500, "Failed to save playlist", err)
return Error(500, "Failed to save playlist", err)
}
playlistDTOs, err := LoadPlaylistItemDTOs(cmd.Id)
if err != nil {
return ApiError(500, "Failed to save playlist", err)
return Error(500, "Failed to save playlist", err)
}
cmd.Result.Items = playlistDTOs
return Json(200, cmd.Result)
return JSON(200, cmd.Result)
}
+32 -34
View File
@@ -11,11 +11,11 @@ import (
"github.com/grafana/grafana/pkg/services/search"
)
func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
func populateDashboardsByID(dashboardByIDs []int64, dashboardIDOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByIds) > 0 {
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
if len(dashboardByIDs) > 0 {
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIDs}
if err := bus.Dispatch(&dashboardQuery); err != nil {
return result, err
}
@@ -26,7 +26,7 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
Slug: item.Slug,
Title: item.Title,
Uri: "db/" + item.Slug,
Order: dashboardIdOrder[item.Id],
Order: dashboardIDOrder[item.Id],
})
}
}
@@ -34,29 +34,27 @@ 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,
}
for _, tag := range dashboardByTag {
searchQuery := search.Query{
Title: "",
Tags: []string{tag},
SignedInUser: signedInUser,
Limit: 100,
IsStarred: false,
OrgId: orgID,
}
if err := bus.Dispatch(&searchQuery); err == nil {
for _, item := range searchQuery.Result {
result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
Title: item.Title,
Uri: item.Uri,
Order: dashboardTagOrder[tag],
})
}
if err := bus.Dispatch(&searchQuery); err == nil {
for _, item := range searchQuery.Result {
result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
Title: item.Title,
Uri: item.Uri,
Order: dashboardTagOrder[tag],
})
}
}
}
@@ -64,19 +62,19 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
return result
}
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := LoadPlaylistItems(playlistId)
func LoadPlaylistDashboards(orgID int64, signedInUser *m.SignedInUser, playlistID int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := LoadPlaylistItems(playlistID)
dashboardByIds := make([]int64, 0)
dashboardByIDs := make([]int64, 0)
dashboardByTag := make([]string, 0)
dashboardIdOrder := make(map[int64]int)
dashboardIDOrder := make(map[int64]int)
dashboardTagOrder := make(map[string]int)
for _, i := range playlistItems {
if i.Type == "dashboard_by_id" {
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
dashboardByIds = append(dashboardByIds, dashboardId)
dashboardIdOrder[dashboardId] = i.Order
dashboardID, _ := strconv.ParseInt(i.Value, 10, 64)
dashboardByIDs = append(dashboardByIDs, dashboardID)
dashboardIDOrder[dashboardID] = i.Order
}
if i.Type == "dashboard_by_tag" {
@@ -87,10 +85,10 @@ func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashb
result := make(dtos.PlaylistDashboardsSlice, 0)
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
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(sort.Reverse(result))
sort.Sort(result)
return result, nil
}
+390
View File
@@ -0,0 +1,390 @@
package pluginproxy
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"text/template"
"time"
"github.com/opentracing/opentracing-go"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
logger = log.New("data-proxy-log")
client = &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
tokenCache = map[int64]*jwtToken{}
)
type jwtToken struct {
ExpiresOn time.Time `json:"-"`
ExpiresOnString string `json:"expires_on"`
AccessToken string `json:"access_token"`
}
type DataSourceProxy struct {
ds *m.DataSource
ctx *m.ReqContext
targetUrl *url.URL
proxyPath string
route *plugins.AppPluginRoute
plugin *plugins.DataSourcePlugin
}
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
targetURL, _ := url.Parse(ds.Url)
return &DataSourceProxy{
ds: ds,
plugin: plugin,
ctx: ctx,
proxyPath: proxyPath,
targetUrl: targetURL,
}
}
func (proxy *DataSourceProxy) HandleRequest() {
if err := proxy.validateRequest(); err != nil {
proxy.ctx.JsonApiErr(403, err.Error(), nil)
return
}
reverseProxy := &httputil.ReverseProxy{
Director: proxy.getDirector(),
FlushInterval: time.Millisecond * 200,
}
var err error
reverseProxy.Transport, err = proxy.ds.GetHttpTransport()
if err != nil {
proxy.ctx.JsonApiErr(400, "Unable to load TLS certificate", err)
return
}
proxy.logRequest()
span, ctx := opentracing.StartSpanFromContext(proxy.ctx.Req.Context(), "datasource reverse proxy")
proxy.ctx.Req.Request = proxy.ctx.Req.WithContext(ctx)
defer span.Finish()
span.SetTag("datasource_id", proxy.ds.Id)
span.SetTag("datasource_type", proxy.ds.Type)
span.SetTag("user_id", proxy.ctx.SignedInUser.UserId)
span.SetTag("org_id", proxy.ctx.SignedInUser.OrgId)
proxy.addTraceFromHeaderValue(span, "X-Panel-Id", "panel_id")
proxy.addTraceFromHeaderValue(span, "X-Dashboard-Id", "dashboard_id")
opentracing.GlobalTracer().Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(proxy.ctx.Req.Request.Header))
reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req.Request)
proxy.ctx.Resp.Header().Del("Set-Cookie")
}
func (proxy *DataSourceProxy) addTraceFromHeaderValue(span opentracing.Span, headerName string, tagName string) {
panelId := proxy.ctx.Req.Header.Get(headerName)
dashId, err := strconv.Atoi(panelId)
if err == nil {
span.SetTag(tagName, dashId)
}
}
func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
return func(req *http.Request) {
req.URL.Scheme = proxy.targetUrl.Scheme
req.URL.Host = proxy.targetUrl.Host
req.Host = proxy.targetUrl.Host
reqQueryVals := req.URL.Query()
if proxy.ds.Type == m.DS_INFLUXDB_08 {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
reqQueryVals.Add("u", proxy.ds.User)
reqQueryVals.Add("p", proxy.ds.Password)
req.URL.RawQuery = reqQueryVals.Encode()
} else if proxy.ds.Type == m.DS_INFLUXDB {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
req.URL.RawQuery = reqQueryVals.Encode()
if !proxy.ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.Password))
}
} else {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
}
if proxy.ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.BasicAuthPassword))
}
dsAuth := req.Header.Get("X-DS-Authorization")
if len(dsAuth) > 0 {
req.Header.Del("X-DS-Authorization")
req.Header.Del("Authorization")
req.Header.Add("Authorization", dsAuth)
}
// 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")
for _, c := range keptCookies {
req.AddCookie(c)
}
// clear X-Forwarded Host/Port/Proto headers
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Forwarded-Proto")
// set X-Forwarded-For header
if req.RemoteAddr != "" {
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
remoteAddr = req.RemoteAddr
}
if req.Header.Get("X-Forwarded-For") != "" {
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
} else {
req.Header.Set("X-Forwarded-For", remoteAddr)
}
}
if proxy.route != nil {
proxy.applyRoute(req)
}
}
}
func (proxy *DataSourceProxy) validateRequest() error {
if proxy.ds.Type == m.DS_INFLUXDB {
if proxy.ctx.Query("db") != proxy.ds.Database {
return errors.New("Datasource is not configured to allow this database")
}
}
if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
return errors.New("Target url is not a valid target")
}
if proxy.ds.Type == m.DS_PROMETHEUS {
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")
}
}
if proxy.ds.Type == m.DS_ES {
if proxy.ctx.Req.Request.Method == "DELETE" {
return errors.New("Deletes not allowed on proxied Elasticsearch datasource")
}
if proxy.ctx.Req.Request.Method == "PUT" {
return errors.New("Puts not allowed on proxied Elasticsearch datasource")
}
if proxy.ctx.Req.Request.Method == "POST" && proxy.proxyPath != "_msearch" {
return errors.New("Posts not allowed on proxied Elasticsearch datasource except on /_msearch")
}
}
// found route if there are any
if len(proxy.plugin.Routes) > 0 {
for _, route := range proxy.plugin.Routes {
// method match
if route.Method != "" && route.Method != "*" && route.Method != proxy.ctx.Req.Method {
continue
}
if route.ReqRole.IsValid() {
if !proxy.ctx.HasUserRole(route.ReqRole) {
return errors.New("Plugin proxy route access denied")
}
}
if strings.HasPrefix(proxy.proxyPath, route.Path) {
proxy.route = route
break
}
}
}
return nil
}
func (proxy *DataSourceProxy) logRequest() {
if !setting.DataProxyLogging {
return
}
var body string
if proxy.ctx.Req.Request.Body != nil {
buffer, err := ioutil.ReadAll(proxy.ctx.Req.Request.Body)
if err == nil {
proxy.ctx.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
body = string(buffer)
}
}
logger.Info("Proxying incoming request",
"userid", proxy.ctx.UserId,
"orgid", proxy.ctx.OrgId,
"username", proxy.ctx.Login,
"datasource", proxy.ds.Type,
"uri", proxy.ctx.Req.RequestURI,
"method", proxy.ctx.Req.Request.Method,
"body", body)
}
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)
return false
}
}
return true
}
func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, proxy.route.Path)
data := templateData{
JsonData: proxy.ds.JsonData.Interface().(map[string]interface{}),
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
}
routeURL, err := url.Parse(proxy.route.Url)
if err != nil {
logger.Error("Error parsing plugin route url")
return
}
req.URL.Scheme = routeURL.Scheme
req.URL.Host = routeURL.Host
req.Host = routeURL.Host
req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxy.proxyPath)
if err := addHeaders(&req.Header, proxy.route, data); err != nil {
logger.Error("Failed to render plugin headers", "error", err)
}
if proxy.route.TokenAuth != nil {
if token, err := proxy.getAccessToken(data); err != nil {
logger.Error("Failed to get access token", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
logger.Info("Requesting", "url", req.URL.String())
}
func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
if cachedToken, found := tokenCache[proxy.ds.Id]; found {
if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
logger.Info("Using token from cache")
return cachedToken.AccessToken, nil
}
}
urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data)
if err != nil {
return "", err
}
params := make(url.Values)
for key, value := range proxy.route.TokenAuth.Params {
interpolatedParam, err := interpolateString(value, data)
if err != nil {
return "", err
}
params.Add(key, interpolatedParam)
}
getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
resp, err := client.Do(getTokenReq)
if err != nil {
return "", err
}
defer resp.Body.Close()
var token jwtToken
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
return "", err
}
expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
tokenCache[proxy.ds.Id] = &token
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
return token.AccessToken, nil
}
func interpolateString(text string, data templateData) (string, error) {
t, err := template.New("content").Parse(text)
if err != nil {
return "", fmt.Errorf("could not parse template %s", text)
}
var contentBuf bytes.Buffer
err = t.Execute(&contentBuf, data)
if err != nil {
return "", fmt.Errorf("failed to execute template %s", text)
}
return contentBuf.String(), nil
}
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
for _, header := range route.Headers {
interpolated, err := interpolateString(header.Content, data)
if err != nil {
return err
}
reqHeaders.Add(header.Name, interpolated)
}
return nil
}
+216
View File
@@ -0,0 +1,216 @@
package pluginproxy
import (
"net/http"
"net/url"
"testing"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/components/simplejson"
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"
. "github.com/smartystreets/goconvey/convey"
)
func TestDSRouteRule(t *testing.T) {
Convey("DataSourceProxy", t, func() {
Convey("Plugin with routes", func() {
plugin := &plugins.DataSourcePlugin{
Routes: []*plugins.AppPluginRoute{
{
Path: "api/v4/",
Url: "https://www.google.com",
ReqRole: m.ROLE_EDITOR,
Headers: []plugins.AppPluginRouteHeader{
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
{
Path: "api/admin",
Url: "https://www.google.com",
ReqRole: m.ROLE_ADMIN,
Headers: []plugins.AppPluginRouteHeader{
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
{
Path: "api/anon",
Url: "https://www.google.com",
Headers: []plugins.AppPluginRouteHeader{
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
},
}
setting.SecretKey = "password"
key, _ := util.Encrypt([]byte("123"), "password")
ds := &m.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"clientId": "asd",
}),
SecureJsonData: map[string][]byte{
"key": key,
},
}
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
ctx := &m.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{Request: req},
},
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
}
Convey("When matching route path", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
proxy.route = plugin.Routes[0]
proxy.applyRoute(req)
Convey("should add headers and update url", func() {
So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
})
})
Convey("Validating request", func() {
Convey("plugin route with valid role", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
err := proxy.validateRequest()
So(err, ShouldBeNil)
})
Convey("plugin route with admin role and user is editor", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
err := proxy.validateRequest()
So(err, ShouldNotBeNil)
})
Convey("plugin route with admin role and user is admin", func() {
ctx.SignedInUser.OrgRole = m.ROLE_ADMIN
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
err := proxy.validateRequest()
So(err, ShouldBeNil)
})
})
})
Convey("When proxying graphite", func() {
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
requestURL, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestURL}
proxy.getDirector()(&req)
Convey("Can translate request url and path", func() {
So(req.URL.Host, ShouldEqual, "graphite:8080")
So(req.URL.Path, ShouldEqual, "/render")
})
})
Convey("When proxying InfluxDB", func() {
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{
Type: m.DS_INFLUXDB_08,
Url: "http://influxdb:8083",
Database: "site",
User: "user",
Password: "password",
}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestURL, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestURL}
proxy.getDirector()(&req)
Convey("Should add db to url", func() {
So(req.URL.Path, ShouldEqual, "/db/site/")
})
Convey("Should add username and password", func() {
queryVals := req.URL.Query()
So(queryVals["u"][0], ShouldEqual, "user")
So(queryVals["p"][0], ShouldEqual, "password")
})
})
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{
"Test": "0asd+asd",
},
}
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
So(err, ShouldBeNil)
So(interpolated, ShouldEqual, "0asd+asd")
})
})
}
+33 -32
View File
@@ -1,18 +1,14 @@
package pluginproxy
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"text/template"
"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"
@@ -23,10 +19,10 @@ type templateData struct {
SecureJsonData map[string]string
}
func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.Header, error) {
func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.Header, error) {
result := http.Header{}
query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appId}
query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
if err := bus.Dispatch(&query); err != nil {
return nil, err
@@ -37,41 +33,44 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
}
for _, header := range route.Headers {
var contentBuf bytes.Buffer
t, err := template.New("content").Parse(header.Content)
if err != nil {
return nil, errors.New(fmt.Sprintf("could not parse header content template for header %s.", header.Name))
}
err = t.Execute(&contentBuf, data)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to execute header content template for header %s.", header.Name))
}
log.Trace("Adding header to proxy request. %s: %s", header.Name, contentBuf.String())
result.Add(header.Name, contentBuf.String())
}
return result, nil
err := addHeaders(&result, route, data)
return result, err
}
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
targetUrl, _ := url.Parse(route.Url)
func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string) *httputil.ReverseProxy {
targetURL, _ := url.Parse(route.Url)
director := func(req *http.Request) {
req.URL.Scheme = targetUrl.Scheme
req.URL.Host = targetUrl.Host
req.Host = targetUrl.Host
req.URL.Scheme = targetURL.Scheme
req.URL.Host = targetURL.Host
req.Host = targetURL.Host
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
req.URL.Path = util.JoinUrlFragments(targetURL.Path, proxyPath)
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")
//Create a HTTP header with the context in it.
// clear X-Forwarded Host/Port/Proto headers
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Forwarded-Proto")
// set X-Forwarded-For header
if req.RemoteAddr != "" {
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
remoteAddr = req.RemoteAddr
}
if req.Header.Get("X-Forwarded-For") != "" {
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
} else {
req.Header.Set("X-Forwarded-For", remoteAddr)
}
}
// Create a HTTP header with the context in it.
ctxJson, err := json.Marshal(ctx.SignedInUser)
if err != nil {
ctx.JsonApiErr(500, "failed to marshal context to json.", err)
@@ -81,7 +80,7 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
req.Header.Add("X-Grafana-Context", string(ctxJson))
if len(route.Headers) > 0 {
headers, err := getHeaders(route, ctx.OrgId, appId)
headers, err := getHeaders(route, ctx.OrgId, appID)
if err != nil {
ctx.JsonApiErr(500, "Could not generate plugin route header", err)
return
@@ -93,6 +92,8 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
}
}
// reqBytes, _ := httputil.DumpRequestOut(req, true);
// log.Trace("Proxying plugin request: %s", string(reqBytes))
}
return &httputil.ReverseProxy{Director: director}
+6 -1
View File
@@ -23,9 +23,14 @@ func TestPluginProxy(t *testing.T) {
setting.SecretKey = "password"
bus.AddHandler("test", func(query *m.GetPluginSettingByIdQuery) error {
key, err := util.Encrypt([]byte("123"), "password")
if err != nil {
return err
}
query.Result = &m.PluginSetting{
SecureJsonData: map[string][]byte{
"key": util.Encrypt([]byte("123"), "password"),
"key": key,
},
}
return nil
+66 -60
View File
@@ -5,13 +5,12 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
func GetPluginList(c *middleware.Context) Response {
func GetPluginList(c *m.ReqContext) Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
embeddedFilter := c.Query("embedded")
@@ -20,7 +19,7 @@ func GetPluginList(c *middleware.Context) Response {
pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
if err != nil {
return ApiError(500, "Failed to get list of plugins", err)
return Error(500, "Failed to get list of plugins", err)
}
result := make(dtos.PluginList, 0)
@@ -48,6 +47,7 @@ func GetPluginList(c *middleware.Context) Response {
LatestVersion: pluginDef.GrafanaNetVersion,
HasUpdate: pluginDef.GrafanaNetHasUpdate,
DefaultNavUrl: pluginDef.DefaultNavUrl,
State: pluginDef.State,
}
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
@@ -75,95 +75,101 @@ func GetPluginList(c *middleware.Context) Response {
}
sort.Sort(result)
return Json(200, result)
return JSON(200, result)
}
func GetPluginSettingById(c *middleware.Context) Response {
pluginId := c.Params(":pluginId")
func GetPluginSettingByID(c *m.ReqContext) Response {
pluginID := c.Params(":pluginId")
if def, exists := plugins.Plugins[pluginId]; !exists {
return ApiError(404, "Plugin not found, no installed plugin with that id", nil)
} else {
dto := &dtos.PluginSetting{
Type: def.Type,
Id: def.Id,
Name: def.Name,
Info: &def.Info,
Dependencies: &def.Dependencies,
Includes: def.Includes,
BaseUrl: def.BaseUrl,
Module: def.Module,
DefaultNavUrl: def.DefaultNavUrl,
LatestVersion: def.GrafanaNetVersion,
HasUpdate: def.GrafanaNetHasUpdate,
}
query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
if err != m.ErrPluginSettingNotFound {
return ApiError(500, "Failed to get login settings", nil)
}
} else {
dto.Enabled = query.Result.Enabled
dto.Pinned = query.Result.Pinned
dto.JsonData = query.Result.JsonData
}
return Json(200, dto)
def, exists := plugins.Plugins[pluginID]
if !exists {
return Error(404, "Plugin not found, no installed plugin with that id", nil)
}
dto := &dtos.PluginSetting{
Type: def.Type,
Id: def.Id,
Name: def.Name,
Info: &def.Info,
Dependencies: &def.Dependencies,
Includes: def.Includes,
BaseUrl: def.BaseUrl,
Module: def.Module,
DefaultNavUrl: def.DefaultNavUrl,
LatestVersion: def.GrafanaNetVersion,
HasUpdate: def.GrafanaNetHasUpdate,
State: def.State,
}
query := m.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
if err != m.ErrPluginSettingNotFound {
return Error(500, "Failed to get login settings", nil)
}
} else {
dto.Enabled = query.Result.Enabled
dto.Pinned = query.Result.Pinned
dto.JsonData = query.Result.JsonData
}
return JSON(200, dto)
}
func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Response {
pluginId := c.Params(":pluginId")
func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response {
pluginID := c.Params(":pluginId")
cmd.OrgId = c.OrgId
cmd.PluginId = pluginId
cmd.PluginId = pluginID
if _, ok := plugins.Apps[cmd.PluginId]; !ok {
return ApiError(404, "Plugin not installed.", nil)
return Error(404, "Plugin not installed.", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update plugin setting", err)
return Error(500, "Failed to update plugin setting", err)
}
return ApiSuccess("Plugin settings updated")
return Success("Plugin settings updated")
}
func GetPluginDashboards(c *middleware.Context) Response {
pluginId := c.Params(":pluginId")
func GetPluginDashboards(c *m.ReqContext) Response {
pluginID := c.Params(":pluginId")
if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil {
list, err := plugins.GetPluginDashboards(c.OrgId, pluginID)
if err != nil {
if notfound, ok := err.(plugins.PluginNotFoundError); ok {
return ApiError(404, notfound.Error(), nil)
return Error(404, notfound.Error(), nil)
}
return ApiError(500, "Failed to get plugin dashboards", err)
} else {
return Json(200, list)
return Error(500, "Failed to get plugin dashboards", err)
}
return JSON(200, list)
}
func GetPluginReadme(c *middleware.Context) Response {
pluginId := c.Params(":pluginId")
func GetPluginMarkdown(c *m.ReqContext) Response {
pluginID := c.Params(":pluginId")
name := c.Params(":name")
if content, err := plugins.GetPluginReadme(pluginId); err != nil {
content, err := plugins.GetPluginMarkdown(pluginID, name)
if err != nil {
if notfound, ok := err.(plugins.PluginNotFoundError); ok {
return ApiError(404, notfound.Error(), nil)
return Error(404, notfound.Error(), nil)
}
return ApiError(500, "Could not get readme", err)
} else {
return Respond(200, content)
return Error(500, "Could not get markdown file", err)
}
resp := Respond(200, content)
resp.Header("Content-Type", "text/plain; charset=utf-8")
return resp
}
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,
@@ -172,8 +178,8 @@ func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand)
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to import dashboard", err)
return Error(500, "Failed to import dashboard", err)
}
return Json(200, cmd.Result)
return JSON(200, cmd.Result)
}
+18 -19
View File
@@ -3,71 +3,70 @@ 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
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to set home dashboard", err)
return Error(500, "Failed to set home dashboard", err)
}
return ApiSuccess("Home dashboard set")
return Success("Home dashboard set")
}
// GET /api/user/preferences
func GetUserPreferences(c *middleware.Context) Response {
func GetUserPreferences(c *m.ReqContext) Response {
return getPreferencesFor(c.OrgId, c.UserId)
}
func getPreferencesFor(orgId int64, userId int64) Response {
prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId}
func getPreferencesFor(orgID int64, userID int64) Response {
prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID}
if err := bus.Dispatch(&prefsQuery); err != nil {
return ApiError(500, "Failed to get preferences", err)
return Error(500, "Failed to get preferences", err)
}
dto := dtos.Prefs{
Theme: prefsQuery.Result.Theme,
HomeDashboardId: prefsQuery.Result.HomeDashboardId,
HomeDashboardID: prefsQuery.Result.HomeDashboardId,
Timezone: prefsQuery.Result.Timezone,
}
return Json(200, &dto)
return JSON(200, &dto)
}
// 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)
}
func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
saveCmd := m.SavePreferencesCommand{
UserId: userId,
OrgId: orgId,
UserId: userID,
OrgId: orgID,
Theme: dtoCmd.Theme,
Timezone: dtoCmd.Timezone,
HomeDashboardId: dtoCmd.HomeDashboardId,
HomeDashboardId: dtoCmd.HomeDashboardID,
}
if err := bus.Dispatch(&saveCmd); err != nil {
return ApiError(500, "Failed to save preferences", err)
return Error(500, "Failed to save preferences", err)
}
return ApiSuccess("Preferences updated")
return Success("Preferences updated")
}
// 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)
}
+18 -19
View File
@@ -2,67 +2,66 @@ 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)
return Error(404, "Quotas not enabled", nil)
}
query := m.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get org quotas", err)
return Error(500, "Failed to get org quotas", err)
}
return Json(200, query.Result)
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)
return Error(404, "Quotas not enabled", nil)
}
cmd.OrgId = c.ParamsInt64(":orgId")
cmd.Target = c.Params(":target")
if _, ok := setting.Quota.Org.ToMap()[cmd.Target]; !ok {
return ApiError(404, "Invalid quota target", nil)
return Error(404, "Invalid quota target", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update org quotas", err)
return Error(500, "Failed to update org quotas", err)
}
return ApiSuccess("Organization quota updated")
return Success("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)
return Error(404, "Quotas not enabled", nil)
}
query := m.GetUserQuotasQuery{UserId: c.ParamsInt64(":id")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get org quotas", err)
return Error(500, "Failed to get org quotas", err)
}
return Json(200, query.Result)
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)
return Error(404, "Quotas not enabled", nil)
}
cmd.UserId = c.ParamsInt64(":id")
cmd.Target = c.Params(":target")
if _, ok := setting.Quota.User.ToMap()[cmd.Target]; !ok {
return ApiError(404, "Invalid quota target", nil)
return Error(404, "Invalid quota target", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update org quotas", err)
return Error(500, "Failed to update org quotas", err)
}
return ApiSuccess("Organization quota updated")
return Success("Organization quota updated")
}
+17 -5
View File
@@ -5,27 +5,39 @@ 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"),
Timeout: queryReader.Get("timeout", "60"),
OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "30"),
UserId: c.UserId,
OrgRole: c.OrgRole,
Timezone: queryReader.Get("tz", ""),
Encoding: queryReader.Get("encoding", ""),
}
pngPath, err := renderer.RenderToPng(renderOpts)
if err != nil && err == renderer.ErrTimeout {
c.Handle(500, err.Error(), err)
return
}
if err != nil {
c.Handle(500, "Failed to render to png", err)
c.Handle(500, "Rendering failed.", err)
return
}
+121
View File
@@ -0,0 +1,121 @@
package api
import (
"net/http"
macaron "gopkg.in/macaron.v1"
)
type Router interface {
Handle(method, pattern string, handlers []macaron.Handler) *macaron.Route
Get(pattern string, handlers ...macaron.Handler) *macaron.Route
}
type RouteRegister interface {
Get(string, ...macaron.Handler)
Post(string, ...macaron.Handler)
Delete(string, ...macaron.Handler)
Put(string, ...macaron.Handler)
Patch(string, ...macaron.Handler)
Any(string, ...macaron.Handler)
Group(string, func(RouteRegister), ...macaron.Handler)
Register(Router) *macaron.Router
}
type RegisterNamedMiddleware func(name string) macaron.Handler
func newRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
return &routeRegister{
prefix: "",
routes: []route{},
subfixHandlers: []macaron.Handler{},
namedMiddleware: namedMiddleware,
}
}
type route struct {
method string
pattern string
handlers []macaron.Handler
}
type routeRegister struct {
prefix string
subfixHandlers []macaron.Handler
namedMiddleware []RegisterNamedMiddleware
routes []route
groups []*routeRegister
}
func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handlers ...macaron.Handler) {
group := &routeRegister{
prefix: rr.prefix + pattern,
subfixHandlers: append(rr.subfixHandlers, handlers...),
routes: []route{},
namedMiddleware: rr.namedMiddleware,
}
fn(group)
rr.groups = append(rr.groups, group)
}
func (rr *routeRegister) Register(router Router) *macaron.Router {
for _, r := range rr.routes {
// GET requests have to be added to macaron routing using Get()
// Otherwise HEAD requests will not be allowed.
// https://github.com/go-macaron/macaron/blob/a325110f8b392bce3e5cdeb8c44bf98078ada3be/router.go#L198
if r.method == http.MethodGet {
router.Get(r.pattern, r.handlers...)
} else {
router.Handle(r.method, r.pattern, r.handlers)
}
}
for _, g := range rr.groups {
g.Register(router)
}
return &macaron.Router{}
}
func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {
h := make([]macaron.Handler, 0)
for _, fn := range rr.namedMiddleware {
h = append(h, fn(pattern))
}
h = append(h, rr.subfixHandlers...)
h = append(h, handlers...)
rr.routes = append(rr.routes, route{
method: method,
pattern: rr.prefix + pattern,
handlers: h,
})
}
func (rr *routeRegister) Get(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodGet, handlers...)
}
func (rr *routeRegister) Post(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodPost, handlers...)
}
func (rr *routeRegister) Delete(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodDelete, handlers...)
}
func (rr *routeRegister) Put(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodPut, handlers...)
}
func (rr *routeRegister) Patch(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodPatch, handlers...)
}
func (rr *routeRegister) Any(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, "*", handlers...)
}
+196
View File
@@ -0,0 +1,196 @@
package api
import (
"net/http"
"strconv"
"testing"
macaron "gopkg.in/macaron.v1"
)
type fakeRouter struct {
route []route
}
func (fr *fakeRouter) Handle(method, pattern string, handlers []macaron.Handler) *macaron.Route {
fr.route = append(fr.route, route{
pattern: pattern,
method: method,
handlers: handlers,
})
return &macaron.Route{}
}
func (fr *fakeRouter) Get(pattern string, handlers ...macaron.Handler) *macaron.Route {
fr.route = append(fr.route, route{
pattern: pattern,
method: http.MethodGet,
handlers: handlers,
})
return &macaron.Route{}
}
func emptyHandlers(n int) []macaron.Handler {
res := []macaron.Handler{}
for i := 1; n >= i; i++ {
res = append(res, emptyHandler(strconv.Itoa(i)))
}
return res
}
func emptyHandler(name string) macaron.Handler {
return struct{ name string }{name: name}
}
func TestRouteSimpleRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(2)},
{method: "GET", pattern: "/down", handlers: emptyHandlers(3)},
}
// Setup
rr := newRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Errorf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
func TestRouteGroupedRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(1)},
{method: "GET", pattern: "/down", handlers: emptyHandlers(2)},
{method: "POST", pattern: "/user", handlers: emptyHandlers(1)},
{method: "PUT", pattern: "/user/friends", handlers: emptyHandlers(1)},
{method: "DELETE", pattern: "/user/admin", handlers: emptyHandlers(2)},
{method: "GET", pattern: "/user/admin/all", handlers: emptyHandlers(4)},
}
// Setup
rr := newRouteRegister()
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
rr.Group("/user", func(user RouteRegister) {
user.Post("", emptyHandler("1"))
user.Put("/friends", emptyHandler("2"))
user.Group("/admin", func(admin RouteRegister) {
admin.Delete("", emptyHandler("3"))
admin.Get("/all", emptyHandler("3"), emptyHandler("4"), emptyHandler("5"))
}, emptyHandler("3"))
})
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Errorf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
func TestNamedMiddlewareRouteRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(2)},
{method: "GET", pattern: "/down", handlers: emptyHandlers(3)},
{method: "POST", pattern: "/user", handlers: emptyHandlers(2)},
{method: "PUT", pattern: "/user/friends", handlers: emptyHandlers(2)},
{method: "DELETE", pattern: "/user/admin", handlers: emptyHandlers(3)},
{method: "GET", pattern: "/user/admin/all", handlers: emptyHandlers(5)},
}
// Setup
rr := newRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
rr.Group("/user", func(user RouteRegister) {
user.Post("", emptyHandler("1"))
user.Put("/friends", emptyHandler("2"))
user.Group("/admin", func(admin RouteRegister) {
admin.Delete("", emptyHandler("3"))
admin.Get("/all", emptyHandler("3"), emptyHandler("4"), emptyHandler("5"))
}, emptyHandler("3"))
})
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Errorf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
+24 -7
View File
@@ -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)
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,
DashboardIds: dbIDs,
Type: dashboardType,
FolderIds: folderIDs,
Permission: permission,
}
err := bus.Dispatch(&searchQuery)
+18 -19
View File
@@ -5,29 +5,28 @@ 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 {
return Json(200, util.DynMap{
func GetSignUpOptions(c *m.ReqContext) Response {
return JSON(200, util.DynMap{
"verifyEmailEnabled": setting.VerifyEmailEnabled,
"autoAssignOrg": setting.AutoAssignOrg,
})
}
// 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)
return Error(401, "User signup is disabled", nil)
}
existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
if err := bus.Dispatch(&existing); err == nil {
return ApiError(422, "User with same email address already exists", nil)
return Error(422, "User with same email address already exists", nil)
}
cmd := m.CreateTempUserCommand{}
@@ -39,7 +38,7 @@ func SignUp(c *middleware.Context, form dtos.SignUpForm) Response {
cmd.RemoteAddr = c.Req.RemoteAddr
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to create signup", err)
return Error(500, "Failed to create signup", err)
}
bus.Publish(&events.SignUpStarted{
@@ -47,14 +46,14 @@ func SignUp(c *middleware.Context, form dtos.SignUpForm) Response {
Code: cmd.Code,
})
metrics.M_Api_User_SignUpStarted.Inc(1)
metrics.M_Api_User_SignUpStarted.Inc()
return Json(200, util.DynMap{"status": "SignUpCreated"})
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)
return Error(401, "User signup is disabled", nil)
}
createUserCmd := m.CreateUserCommand{
@@ -76,12 +75,12 @@ func SignUpStep2(c *middleware.Context, form dtos.SignUpStep2Form) Response {
// check if user exists
existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
if err := bus.Dispatch(&existing); err == nil {
return ApiError(401, "User with same email address already exists", nil)
return Error(401, "User with same email address already exists", nil)
}
// dispatch create command
if err := bus.Dispatch(&createUserCmd); err != nil {
return ApiError(500, "Failed to create user", err)
return Error(500, "Failed to create user", err)
}
// publish signup event
@@ -99,7 +98,7 @@ func SignUpStep2(c *middleware.Context, form dtos.SignUpStep2Form) Response {
// check for pending invites
invitesQuery := m.GetTempUsersQuery{Email: form.Email, Status: m.TmpUserInvitePending}
if err := bus.Dispatch(&invitesQuery); err != nil {
return ApiError(500, "Failed to query database for invites", err)
return Error(500, "Failed to query database for invites", err)
}
apiResponse := util.DynMap{"message": "User sign up completed successfully", "code": "redirect-to-landing-page"}
@@ -111,9 +110,9 @@ func SignUpStep2(c *middleware.Context, form dtos.SignUpStep2Form) Response {
}
loginUserWithUser(user, c)
metrics.M_Api_User_SignUpCompleted.Inc(1)
metrics.M_Api_User_SignUpCompleted.Inc()
return Json(200, apiResponse)
return JSON(200, apiResponse)
}
func verifyUserSignUpEmail(email string, code string) (bool, Response) {
@@ -121,14 +120,14 @@ func verifyUserSignUpEmail(email string, code string) (bool, Response) {
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrTempUserNotFound {
return false, ApiError(404, "Invalid email verification code", nil)
return false, Error(404, "Invalid email verification code", nil)
}
return false, ApiError(500, "Failed to read temp user", err)
return false, Error(500, "Failed to read temp user", err)
}
tempUser := query.Result
if tempUser.Email != email {
return false, ApiError(404, "Email verification code does not match email", nil)
return false, Error(404, "Email verification code does not match email", nil)
}
return true, nil
+9 -10
View File
@@ -2,39 +2,38 @@ 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)
return Error(412, "You need to sign in to star dashboards", nil)
}
cmd := m.StarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
if cmd.DashboardId <= 0 {
return ApiError(400, "Missing dashboard id", nil)
return Error(400, "Missing dashboard id", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to star dashboard", err)
return Error(500, "Failed to star dashboard", err)
}
return ApiSuccess("Dashboard starred!")
return Success("Dashboard starred!")
}
func UnstarDashboard(c *middleware.Context) Response {
func UnstarDashboard(c *m.ReqContext) Response {
cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
if cmd.DashboardId <= 0 {
return ApiError(400, "Missing dashboard id", nil)
return Error(400, "Missing dashboard id", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to unstar dashboard", err)
return Error(500, "Failed to unstar dashboard", err)
}
return ApiSuccess("Dashboard unstarred")
return Success("Dashboard unstarred")
}
+97
View 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 Error(409, "Team name taken", err)
}
return Error(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 Error(400, "Team name taken", err)
}
return Error(500, "Failed to update Team", err)
}
return Success("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 Error(404, "Failed to delete Team. ID not found", nil)
}
return Error(500, "Failed to update Team", err)
}
return Success("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 Error(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 Error(404, "Team not found", err)
}
return Error(500, "Failed to get Team", err)
}
return JSON(200, &query.Result)
}
+61
View 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 Error(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 Error(404, "Team not found", nil)
}
if err == m.ErrTeamMemberAlreadyAdded {
return Error(400, "User is already added to this team", nil)
}
return Error(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 Error(404, "Team not found", nil)
}
if err == m.ErrTeamMemberNotFound {
return Error(404, "Team member not found", nil)
}
return Error(500, "Failed to remove Member from Team", err)
}
return Success("Team Member removed")
}
+71
View 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)
})
})
})
}
+73 -68
View File
@@ -1,47 +1,48 @@
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"))
}
func getUserUserProfile(userId int64) Response {
query := m.GetUserProfileQuery{UserId: userId}
func getUserUserProfile(userID int64) Response {
query := m.GetUserProfileQuery{UserId: userID}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrUserNotFound {
return ApiError(404, m.ErrUserNotFound.Error(), nil)
return Error(404, m.ErrUserNotFound.Error(), nil)
}
return ApiError(500, "Failed to get user", err)
return Error(500, "Failed to get user", err)
}
return Json(200, query.Result)
return JSON(200, query.Result)
}
// 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 {
return ApiError(404, m.ErrUserNotFound.Error(), nil)
return Error(404, m.ErrUserNotFound.Error(), nil)
}
return ApiError(500, "Failed to get user", err)
return Error(500, "Failed to get user", err)
}
user := query.Result
result := m.UserProfileDTO{
Id: user.Id,
Name: user.Name,
Email: user.Email,
Login: user.Login,
@@ -49,17 +50,17 @@ func GetUserByLoginOrEmail(c *middleware.Context) Response {
IsGrafanaAdmin: user.IsAdmin,
OrgId: user.OrgId,
}
return Json(200, &result)
return JSON(200, &result)
}
// 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)
return Error(400, "Not allowed to change email when auth proxy is using email property", nil)
}
if setting.AuthProxyHeaderProperty == "username" && cmd.Login != c.Login {
return ApiError(400, "Not allowed to change username when auth proxy is using username property", nil)
return Error(400, "Not allowed to change username when auth proxy is using username property", nil)
}
}
cmd.UserId = c.UserId
@@ -67,66 +68,66 @@ 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 {
userId := c.ParamsInt64(":id")
orgId := c.ParamsInt64(":orgId")
func UpdateUserActiveOrg(c *m.ReqContext) Response {
userID := c.ParamsInt64(":id")
orgID := c.ParamsInt64(":orgId")
if !validateUsingOrg(userId, orgId) {
return ApiError(401, "Not a valid organization", nil)
if !validateUsingOrg(userID, orgID) {
return Error(401, "Not a valid organization", nil)
}
cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
cmd := m.SetUsingOrgCommand{UserId: userID, OrgId: orgID}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to change active organization", err)
return Error(500, "Failed to change active organization", err)
}
return ApiSuccess("Active organization changed")
return Success("Active organization changed")
}
func handleUpdateUser(cmd m.UpdateUserCommand) Response {
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
return ApiError(400, "Validation error, need to specify either username or email", nil)
return Error(400, "Validation error, need to specify either username or email", nil)
}
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update user", err)
return Error(500, "Failed to update user", err)
}
return ApiSuccess("User updated")
return Success("User updated")
}
// 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"))
}
func getUserOrgList(userId int64) Response {
query := m.GetUserOrgListQuery{UserId: userId}
func getUserOrgList(userID int64) Response {
query := m.GetUserOrgListQuery{UserId: userID}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get user organizations", err)
return Error(500, "Failed to get user organizations", err)
}
return Json(200, query.Result)
return JSON(200, query.Result)
}
func validateUsingOrg(userId int64, orgId int64) bool {
query := m.GetUserOrgListQuery{UserId: userId}
func validateUsingOrg(userID int64, orgID int64) bool {
query := m.GetUserOrgListQuery{UserId: userID}
if err := bus.Dispatch(&query); err != nil {
return false
@@ -135,7 +136,7 @@ func validateUsingOrg(userId int64, orgId int64) bool {
// validate that the org id in the list
valid := false
for _, other := range query.Result {
if other.OrgId == orgId {
if other.OrgId == orgID {
valid = true
}
}
@@ -144,31 +145,31 @@ func validateUsingOrg(userId int64, orgId int64) bool {
}
// POST /api/user/using/:id
func UserSetUsingOrg(c *middleware.Context) Response {
orgId := c.ParamsInt64(":id")
func UserSetUsingOrg(c *m.ReqContext) Response {
orgID := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) {
return ApiError(401, "Not a valid organization", nil)
if !validateUsingOrg(c.UserId, orgID) {
return Error(401, "Not a valid organization", nil)
}
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to change active organization", err)
return Error(500, "Failed to change active organization", err)
}
return ApiSuccess("Active organization changed")
return Success("Active organization changed")
}
// GET /profile/switch-org/:id
func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) {
orgId := c.ParamsInt64(":id")
func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
orgID := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) {
if !validateUsingOrg(c.UserId, orgID) {
NotFoundHandler(c)
}
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID}
if err := bus.Dispatch(&cmd); err != nil {
NotFoundHandler(c)
@@ -177,58 +178,58 @@ 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)
return Error(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil)
}
userQuery := m.GetUserByIdQuery{Id: c.UserId}
if err := bus.Dispatch(&userQuery); err != nil {
return ApiError(500, "Could not read user from database", err)
return Error(500, "Could not read user from database", err)
}
passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt)
if passwordHashed != userQuery.Result.Password {
return ApiError(401, "Invalid old password", nil)
return Error(401, "Invalid old password", nil)
}
password := m.Password(cmd.NewPassword)
if password.IsWeak() {
return ApiError(400, "New password is too short", nil)
return Error(400, "New password is too short", nil)
}
cmd.UserId = c.UserId
cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt)
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to change user password", err)
return Error(500, "Failed to change user password", err)
}
return ApiSuccess("User password changed")
return Success("User password changed")
}
// 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)
return Error(500, "Failed to fetch users", err)
}
return Json(200, query.Result.Users)
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)
return Error(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
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
@@ -246,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
@@ -264,21 +269,21 @@ func SetHelpFlag(c *middleware.Context) Response {
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update help flag", err)
return Error(500, "Failed to update help flag", err)
}
return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
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),
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update help flag", err)
return Error(500, "Failed to update help flag", err)
}
return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
return JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
}
@@ -19,6 +19,7 @@ type CommandLine interface {
PluginDirectory() string
RepoDirectory() string
PluginURL() string
}
type contextCommandLine struct {
@@ -44,3 +45,7 @@ func (c *contextCommandLine) PluginDirectory() string {
func (c *contextCommandLine) RepoDirectory() string {
return c.GlobalString("repo")
}
func (c *contextCommandLine) PluginURL() string {
return c.GlobalString("pluginUrl")
}
+13 -7
View File
@@ -11,22 +11,18 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
func runDbCommand(command func(commandLine CommandLine) error) func(context *cli.Context) {
return func(context *cli.Context) {
cmd := &contextCommandLine{context}
flag.Parse()
setting.NewConfigContext(&setting.CommandLineArgs{
Config: *configFile,
HomePath: *homePath,
Config: cmd.String("config"),
HomePath: cmd.String("homepath"),
Args: flag.Args(),
})
sqlstore.NewEngine()
cmd := &contextCommandLine{context}
if err := command(cmd); err != nil {
logger.Errorf("\n%s: ", color.RedString("Error"))
logger.Errorf("%s\n\n", err)
@@ -95,6 +91,16 @@ var adminCommands = []cli.Command{
Name: "reset-admin-password",
Usage: "reset-admin-password <new password>",
Action: runDbCommand(resetPasswordCommand),
Flags: []cli.Flag{
cli.StringFlag{
Name: "homepath",
Usage: "path to grafana install/home path, defaults to working directory",
},
cli.StringFlag{
Name: "config",
Usage: "path to config file",
},
},
},
}
@@ -101,3 +101,7 @@ func (fcli *FakeCommandLine) RepoDirectory() string {
func (fcli *FakeCommandLine) PluginDirectory() string {
return fcli.GlobalString("pluginsDir")
}
func (fcli *FakeCommandLine) PluginURL() string {
return fcli.GlobalString("pluginUrl")
}
+23 -21
View File
@@ -58,41 +58,43 @@ func installCommand(c CommandLine) error {
}
func InstallPlugin(pluginName, version string, c CommandLine) error {
plugin, err := s.GetPlugin(pluginName, c.RepoDirectory())
pluginFolder := c.PluginDirectory()
if err != nil {
return err
downloadURL := c.PluginURL()
if downloadURL == "" {
plugin, err := s.GetPlugin(pluginName, c.RepoDirectory())
if err != nil {
return err
}
v, err := SelectVersion(plugin, version)
if err != nil {
return err
}
if version == "" {
version = v.Version
}
downloadURL = fmt.Sprintf("%s/%s/versions/%s/download",
c.GlobalString("repo"),
pluginName,
version)
}
v, err := SelectVersion(plugin, version)
if err != nil {
return err
}
if version == "" {
version = v.Version
}
downloadURL := fmt.Sprintf("%s/%s/versions/%s/download",
c.GlobalString("repo"),
pluginName,
version)
logger.Infof("installing %v @ %v\n", plugin.Id, version)
logger.Infof("installing %v @ %v\n", pluginName, version)
logger.Infof("from url: %v\n", downloadURL)
logger.Infof("into: %v\n", pluginFolder)
logger.Info("\n")
err = downloadFile(plugin.Id, pluginFolder, downloadURL)
err := downloadFile(pluginName, pluginFolder, downloadURL)
if err != nil {
return err
}
logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), plugin.Id)
logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), pluginName)
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 -2
View File
@@ -17,8 +17,6 @@ var version = "master"
func main() {
setupLogging()
services.Init(version)
app := cli.NewApp()
app.Name = "Grafana cli"
app.Usage = ""
@@ -38,12 +36,26 @@ func main() {
Value: "https://grafana.com/api/plugins",
EnvVar: "GF_PLUGIN_REPO",
},
cli.StringFlag{
Name: "pluginUrl",
Usage: "Full url to the plugin zip file instead of downloading the plugin from grafana.com/api",
Value: "",
EnvVar: "GF_PLUGIN_URL",
},
cli.BoolFlag{
Name: "insecure",
Usage: "Skip TLS verification (insecure)",
},
cli.BoolFlag{
Name: "debug, d",
Usage: "enable debug logging",
},
}
app.Before = func(c *cli.Context) error {
services.Init(version, c.GlobalBool("insecure"))
return nil
}
app.Commands = commands.Commands
app.CommandNotFound = cmdNotFound
+5 -3
View File
@@ -22,7 +22,7 @@ var (
grafanaVersion string
)
func Init(version string) {
func Init(version string, skipTLSVerify bool) {
grafanaVersion = version
tr := &http.Transport{
@@ -30,13 +30,15 @@ func Init(version string) {
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipTLSVerify,
},
}
HttpClient = http.Client{
+54 -52
View File
@@ -3,31 +3,34 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/trace"
"strconv"
"syscall"
"time"
"net/http"
_ "net/http/pprof"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/setting"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
_ "github.com/grafana/grafana/pkg/tsdb/influxdb"
_ "github.com/grafana/grafana/pkg/tsdb/mqe"
_ "github.com/grafana/grafana/pkg/tsdb/mysql"
_ "github.com/grafana/grafana/pkg/tsdb/opentsdb"
_ "github.com/grafana/grafana/pkg/tsdb/postgres"
_ "github.com/grafana/grafana/pkg/tsdb/prometheus"
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
)
var version = "4.1.0"
var version = "5.0.0"
var commit = "NA"
var buildstamp string
var build_date string
@@ -37,18 +40,35 @@ 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() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
func main() {
v := flag.Bool("v", false, "prints current version and exits")
profile := flag.Bool("profile", false, "Turn on pprof profiling")
profilePort := flag.Int("profile-port", 6060, "Define custom port for profiling")
flag.Parse()
if *v {
fmt.Printf("Version %s (commit: %s)\n", version, commit)
os.Exit(0)
}
if *profile {
runtime.SetBlockProfileRate(1)
go func() {
http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
}()
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
}
buildstampInt64, _ := strconv.ParseInt(buildstamp, 10, 64)
if buildstampInt64 == 0 {
buildstampInt64 = time.Now().Unix()
@@ -58,51 +78,29 @@ func main() {
setting.BuildCommit = commit
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 initRuntime() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
Config: *configFile,
HomePath: *homePath,
Args: flag.Args(),
})
if err != nil {
log.Fatal(3, err.Error())
}
logger := log.New("main")
logger.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
setting.LogConfigurationInfo()
}
func initSql() {
sqlstore.NewEngine()
sqlstore.EnsureAdminUser()
}
func writePIDFile() {
if *pidFile == "" {
return
}
// Ensure the required directory structure exists.
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
if err != nil {
log.Fatal(3, "Failed to verify pid directory", err)
}
// Retrieve the PID and write it.
pid := strconv.Itoa(os.Getpid())
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
log.Fatal(3, "Failed to write pidfile", err)
}
}
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
@@ -112,8 +110,12 @@ func listenToSystemSignals(server models.GrafanaServer) {
select {
case sig := <-signalChan:
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
}
}
+115 -24
View File
@@ -2,7 +2,16 @@ 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"
@@ -10,18 +19,19 @@ import (
"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)
@@ -39,21 +49,35 @@ type GrafanaServerImpl struct {
childRoutines *errgroup.Group
log log.Logger
httpServer *api.HttpServer
httpServer *api.HTTPServer
}
func (g *GrafanaServerImpl) Start() {
go listenToSystemSignals(g)
func (g *GrafanaServerImpl) Start() error {
g.initLogging()
g.writePIDFile()
writePIDFile()
initRuntime()
initSql()
metrics.Init()
metrics.Init(setting.Cfg)
search.Init()
login.Init()
social.NewOAuthService()
eventpublisher.Init()
plugins.Init()
pluginManager, err := plugins.NewPluginManager(g.context)
if err != nil {
return fmt.Errorf("Failed to start plugins. error: %v", err)
}
g.childRoutines.Go(func() error { return pluginManager.Run(g.context) })
if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil {
return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
}
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 {
@@ -65,25 +89,46 @@ func (g *GrafanaServerImpl) Start() {
cleanUpService := cleanup.NewCleanUpService()
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
if err := notifications.Init(); err != nil {
g.log.Error("Notification service failed to initialize", "erro", err)
g.Shutdown(1, "Startup failed")
return
if err = notifications.Init(); err != nil {
return fmt.Errorf("Notification service failed to initialize. error: %v", err)
}
g.startHttpServer()
sendSystemdNotification("READY=1")
return g.startHttpServer()
}
func (g *GrafanaServerImpl) startHttpServer() {
g.httpServer = api.NewHttpServer()
func initSql() {
sqlstore.NewEngine()
sqlstore.EnsureAdminUser()
}
func (g *GrafanaServerImpl) initLogging() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
Config: *configFile,
HomePath: *homePath,
Args: flag.Args(),
})
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to start grafana. error: %s\n", err.Error())
os.Exit(1)
}
g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
setting.LogConfigurationInfo()
}
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) {
@@ -96,8 +141,54 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
g.shutdownFn()
err = g.childRoutines.Wait()
if err != nil && err != context.Canceled {
g.log.Error("Server shutdown completed with an error", "error", err)
}
}
g.log.Info("Shutdown completed", "reason", err)
log.Close()
os.Exit(code)
func (g *GrafanaServerImpl) writePIDFile() {
if *pidFile == "" {
return
}
// Ensure the required directory structure exists.
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
if err != nil {
g.log.Error("Failed to verify pid directory", "error", err)
os.Exit(1)
}
// Retrieve the PID and write it.
pid := strconv.Itoa(os.Getpid())
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
g.log.Error("Failed to write pidfile", "error", err)
os.Exit(1)
}
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
}
+149
View File
@@ -0,0 +1,149 @@
package dashdiffs
import (
"encoding/json"
"errors"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
diff "github.com/yudai/gojsondiff"
deltaFormatter "github.com/yudai/gojsondiff/formatter"
)
var (
// ErrUnsupportedDiffType occurs when an invalid diff type is used.
ErrUnsupportedDiffType = errors.New("dashdiff: unsupported diff type")
// ErrNilDiff occurs when two compared interfaces are identical.
ErrNilDiff = errors.New("dashdiff: diff is nil")
diffLogger = log.New("dashdiffs")
)
type DiffType int
const (
DiffJSON DiffType = iota
DiffBasic
DiffDelta
)
type Options struct {
OrgId int64
Base DiffTarget
New DiffTarget
DiffType DiffType
}
type DiffTarget struct {
DashboardId int64
Version int
UnsavedDashboard *simplejson.Json
}
type Result struct {
Delta []byte `json:"delta"`
}
func ParseDiffType(diff string) DiffType {
switch diff {
case "json":
return DiffJSON
case "basic":
return DiffBasic
case "delta":
return DiffDelta
}
return DiffBasic
}
// CompareDashboardVersionsCommand computes the JSON diff of two versions,
// assigning the delta of the diff to the `Delta` field.
func CalculateDiff(options *Options) (*Result, error) {
baseVersionQuery := models.GetDashboardVersionQuery{
DashboardId: options.Base.DashboardId,
Version: options.Base.Version,
OrgId: options.OrgId,
}
if err := bus.Dispatch(&baseVersionQuery); err != nil {
return nil, err
}
newVersionQuery := models.GetDashboardVersionQuery{
DashboardId: options.New.DashboardId,
Version: options.New.Version,
OrgId: options.OrgId,
}
if err := bus.Dispatch(&newVersionQuery); err != nil {
return nil, err
}
baseData := baseVersionQuery.Result.Data
newData := newVersionQuery.Result.Data
left, jsonDiff, err := getDiff(baseData, newData)
if err != nil {
return nil, err
}
result := &Result{}
switch options.DiffType {
case DiffDelta:
deltaOutput, err := deltaFormatter.NewDeltaFormatter().Format(jsonDiff)
if err != nil {
return nil, err
}
result.Delta = []byte(deltaOutput)
case DiffJSON:
jsonOutput, err := NewJSONFormatter(left).Format(jsonDiff)
if err != nil {
return nil, err
}
result.Delta = []byte(jsonOutput)
case DiffBasic:
basicOutput, err := NewBasicFormatter(left).Format(jsonDiff)
if err != nil {
return nil, err
}
result.Delta = basicOutput
default:
return nil, ErrUnsupportedDiffType
}
return result, nil
}
// getDiff computes the diff of two dashboard versions.
func getDiff(baseData, newData *simplejson.Json) (interface{}, diff.Diff, error) {
leftBytes, err := baseData.Encode()
if err != nil {
return nil, nil, err
}
rightBytes, err := newData.Encode()
if err != nil {
return nil, nil, err
}
jsonDiff, err := diff.New().Compare(leftBytes, rightBytes)
if err != nil {
return nil, nil, err
}
if !jsonDiff.Modified() {
return nil, nil, ErrNilDiff
}
left := make(map[string]interface{})
err = json.Unmarshal(leftBytes, &left)
return left, jsonDiff, nil
}
+425
View File
@@ -0,0 +1,425 @@
package dashdiffs
import (
"bytes"
"html/template"
diff "github.com/yudai/gojsondiff"
)
// A BasicDiff holds the stateful values that are used when generating a basic
// diff from JSON tokens.
type BasicDiff struct {
narrow string
keysIdent int
writing bool
LastIndent int
Block *BasicBlock
Change *BasicChange
Summary *BasicSummary
}
// A BasicBlock represents a top-level element in a basic diff.
type BasicBlock struct {
Title string
Old interface{}
New interface{}
Change ChangeType
Changes []*BasicChange
Summaries []*BasicSummary
LineStart int
LineEnd int
}
// A BasicChange represents the change from an old to new value. There are many
// BasicChanges in a BasicBlock.
type BasicChange struct {
Key string
Old interface{}
New interface{}
Change ChangeType
LineStart int
LineEnd int
}
// A BasicSummary represents the changes within a basic block that're too deep
// or verbose to be represented in the top-level BasicBlock element, or in the
// BasicChange. Instead of showing the values in this case, we simply print
// the key and count how many times the given change was applied to that
// element.
type BasicSummary struct {
Key string
Change ChangeType
Count int
LineStart int
LineEnd int
}
type BasicFormatter struct {
jsonDiff *JSONFormatter
tpl *template.Template
}
func NewBasicFormatter(left interface{}) *BasicFormatter {
tpl := template.Must(template.New("block").Funcs(tplFuncMap).Parse(tplBlock))
tpl = template.Must(tpl.New("change").Funcs(tplFuncMap).Parse(tplChange))
tpl = template.Must(tpl.New("summary").Funcs(tplFuncMap).Parse(tplSummary))
return &BasicFormatter{
jsonDiff: NewJSONFormatter(left),
tpl: tpl,
}
}
// Format takes the diff of two JSON documents, and returns the difference
// between them summarized in an HTML document.
func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
// calling jsonDiff.Format(d) populates the JSON diff's "Lines" value,
// which we use to compute the basic dif
_, err := b.jsonDiff.Format(d)
if err != nil {
return nil, err
}
bd := &BasicDiff{}
blocks := bd.Basic(b.jsonDiff.Lines)
buf := &bytes.Buffer{}
err = b.tpl.ExecuteTemplate(buf, "block", blocks)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Basic transforms a slice of JSONLines into a slice of BasicBlocks.
func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
// init an array you can append to for the basic "blocks"
blocks := make([]*BasicBlock, 0)
for _, line := range lines {
if b.returnToTopLevelKey(line) {
if b.Block != nil {
blocks = append(blocks, b.Block)
}
}
// Record the last indent level at each pass in case we need to
// check for a change in depth inside the JSON data structures.
b.LastIndent = line.Indent
if line.Indent == 1 {
if block, ok := b.handleTopLevelChange(line); ok {
blocks = append(blocks, block)
}
}
// Here is where we handle changes for all types, appending each change
// to the current block based on the value.
//
// Values which only occupy a single line in JSON (like a string or
// int, for example) are treated as "Basic Changes" that we append to
// the current block as soon as they're detected.
//
// Values which occupy multiple lines (either slices or maps) are
// treated as "Basic Summaries". When we detect the "ChangeNil" type,
// we know we've encountered one of these types, so we record the
// starting position as well the type of the change, and stop
// performing comparisons until we find the end of that change. Upon
// finding the change, we append it to the current block, and begin
// performing comparisons again.
if line.Indent > 1 {
// check to ensure a single line change
if b.isSingleLineChange(line) {
switch line.Change {
case ChangeAdded, ChangeDeleted:
b.Block.Changes = append(b.Block.Changes, &BasicChange{
Key: line.Key,
Change: line.Change,
New: line.Val,
LineStart: line.LineNum,
})
case ChangeOld:
b.Change = &BasicChange{
Key: line.Key,
Change: line.Change,
Old: line.Val,
LineStart: line.LineNum,
}
case ChangeNew:
b.Change.New = line.Val
b.Change.LineEnd = line.LineNum
b.Block.Changes = append(b.Block.Changes, b.Change)
default:
//ok
}
// otherwise, we're dealing with a change at a deeper level. We
// know there's a change somewhere in the JSON tree, but we
// don't know exactly where, so we go deeper.
} else {
// if the change is anything but unchanged, continue processing
//
// we keep "narrowing" the key as we go deeper, in order to
// correctly report the key name for changes found within an
// object or array.
if line.Change != ChangeUnchanged {
if line.Key != "" {
b.narrow = line.Key
b.keysIdent = line.Indent
}
// if the change isn't nil, and we're not already writing
// out a change, then we've found something.
//
// First, try to determine the title of the embedded JSON
// object. If it's an empty string, then we're in an object
// or array, so we default to using the "narrowed" key.
//
// We also start recording the basic summary, until we find
// the next `ChangeUnchanged`.
if line.Change != ChangeNil {
if !b.writing {
b.writing = true
key := b.Block.Title
if b.narrow != "" {
key = b.narrow
if b.keysIdent > line.Indent {
key = b.Block.Title
}
}
b.Summary = &BasicSummary{
Key: key,
Change: line.Change,
LineStart: line.LineNum,
}
}
}
// if we find a `ChangeUnchanged`, we do one of two things:
//
// - if we're recording a change already, then we know
// we've come to the end of that change block, so we write
// that change out be recording the line number of where
// that change ends, and append it to the current block's
// summary.
//
// - if we're not recording a change, then we do nothing,
// since the BasicDiff doesn't report on unchanged JSON
// values.
} else {
if b.writing {
b.writing = false
b.Summary.LineEnd = line.LineNum
b.Block.Summaries = append(b.Block.Summaries, b.Summary)
}
}
}
}
}
return blocks
}
// returnToTopLevelKey indicates that we've moved from a key at one level deep
// in the JSON document to a top level key.
//
// In order to produce distinct "blocks" when rendering the basic diff,
// we need a way to distinguish between different sections of data.
// To do this, we consider the value(s) of each top-level JSON key to
// represent a distinct block for Grafana's JSON data structure, so
// we perform this check to see if we've entered a new "block". If we
// have, we simply append the existing block to the array of blocks.
func (b *BasicDiff) returnToTopLevelKey(line *JSONLine) bool {
return b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil
}
// handleTopLevelChange handles a change on one of the top-level keys on a JSON
// document.
//
// If the line's indentation is at level 1, then we know it's a top
// level key in the JSON document. As mentioned earlier, we treat these
// specially as they indicate their values belong to distinct blocks.
//
// At level 1, we only record single-line changes, ie, the "added",
// "deleted", "old" or "new" cases, since we know those values aren't
// arrays or maps. We only handle these cases at level 2 or deeper,
// since for those we either output a "change" or "summary". This is
// done for formatting reasons only, so we have logical "blocks" to
// display.
func (b *BasicDiff) handleTopLevelChange(line *JSONLine) (*BasicBlock, bool) {
switch line.Change {
case ChangeNil:
if line.Change == ChangeNil {
if line.Key != "" {
b.Block = &BasicBlock{
Title: line.Key,
Change: line.Change,
}
}
}
case ChangeAdded, ChangeDeleted:
return &BasicBlock{
Title: line.Key,
Change: line.Change,
New: line.Val,
LineStart: line.LineNum,
}, true
case ChangeOld:
b.Block = &BasicBlock{
Title: line.Key,
Old: line.Val,
Change: line.Change,
LineStart: line.LineNum,
}
case ChangeNew:
b.Block.New = line.Val
b.Block.LineEnd = line.LineNum
// For every "old" change there is a corresponding "new", which
// is why we wait until we detect the "new" change before
// appending the change.
return b.Block, true
default:
// ok
}
return nil, false
}
// isSingleLineChange ensures we're iterating over a single line change (ie,
// either a single line or a old-new value pair was changed in the JSON file).
func (b *BasicDiff) isSingleLineChange(line *JSONLine) bool {
return line.Key != "" && line.Val != nil && !b.writing
}
// encStateMap is used in the template helper
var (
encStateMap = map[ChangeType]string{
ChangeAdded: "added",
ChangeDeleted: "deleted",
ChangeOld: "changed",
ChangeNew: "changed",
}
// tplFuncMap is the function map for each template
tplFuncMap = template.FuncMap{
"getChange": func(c ChangeType) string {
state, ok := encStateMap[c]
if !ok {
return "changed"
}
return state
},
}
)
var (
// tplBlock is the container for the basic diff. It iterates over each
// basic block, expanding each "change" and "summary" belonging to every
// block.
tplBlock = `{{ define "block" -}}
{{ range . }}
<div class="diff-group">
<div class="diff-block">
<h2 class="diff-block-title">
<i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle"></i>
<strong class="diff-title">{{ .Title }}</strong> {{ getChange .Change }}
</h2>
<!-- Overview -->
{{ if .Old }}
<div class="diff-label">{{ .Old }}</div>
<i class="diff-arrow fa fa-long-arrow-right"></i>
{{ end }}
{{ if .New }}
<div class="diff-label">{{ .New }}</div>
{{ end }}
{{ if .LineStart }}
<diff-link-json
line-link="{{ .LineStart }}"
line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
switch-view="ctrl.getDiff('html')"
/>
{{ end }}
</div>
<!-- Basic Changes -->
{{ range .Changes }}
<ul class="diff-change-container">
{{ template "change" . }}
</ul>
{{ end }}
<!-- Basic Summary -->
{{ range .Summaries }}
{{ template "summary" . }}
{{ end }}
</div>
{{ end }}
{{ end }}`
// tplChange is the template for basic changes.
tplChange = `{{ define "change" -}}
<li class="diff-change-group">
<span class="bullet-position-container">
<div class="diff-change-item diff-change-title">{{ getChange .Change }} {{ .Key }}</div>
<div class="diff-change-item">
{{ if .Old }}
<div class="diff-label">{{ .Old }}</div>
<i class="diff-arrow fa fa-long-arrow-right"></i>
{{ end }}
{{ if .New }}
<div class="diff-label">{{ .New }}</div>
{{ end }}
</div>
{{ if .LineStart }}
<diff-link-json
line-link="{{ .LineStart }}"
line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
switch-view="ctrl.getDiff('json')"
/>
{{ end }}
</span>
</li>
{{ end }}`
// tplSummary is for basic summaries.
tplSummary = `{{ define "summary" -}}
<div class="diff-group-name">
<i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle-o diff-list-circle"></i>
{{ if .Count }}
<strong>{{ .Count }}</strong>
{{ end }}
{{ if .Key }}
<strong class="diff-summary-key">{{ .Key }}</strong>
{{ getChange .Change }}
{{ end }}
{{ if .LineStart }}
<diff-link-json
line-link="{{ .LineStart }}"
line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
switch-view="ctrl.getDiff('json')"
/>
{{ end }}
</div>
{{ end }}`
)
+477
View File
@@ -0,0 +1,477 @@
package dashdiffs
import (
"bytes"
"errors"
"fmt"
"html/template"
"sort"
diff "github.com/yudai/gojsondiff"
)
type ChangeType int
const (
ChangeNil ChangeType = iota
ChangeAdded
ChangeDeleted
ChangeOld
ChangeNew
ChangeUnchanged
)
var (
// changeTypeToSymbol is used for populating the terminating characer in
// the diff
changeTypeToSymbol = map[ChangeType]string{
ChangeNil: "",
ChangeAdded: "+",
ChangeDeleted: "-",
ChangeOld: "-",
ChangeNew: "+",
}
// changeTypeToName is used for populating class names in the diff
changeTypeToName = map[ChangeType]string{
ChangeNil: "same",
ChangeAdded: "added",
ChangeDeleted: "deleted",
ChangeOld: "old",
ChangeNew: "new",
}
)
var (
// tplJSONDiffWrapper is the template that wraps a diff
tplJSONDiffWrapper = `{{ define "JSONDiffWrapper" -}}
{{ range $index, $element := . }}
{{ template "JSONDiffLine" $element }}
{{ end }}
{{ end }}`
// tplJSONDiffLine is the template that prints each line in a diff
tplJSONDiffLine = `{{ define "JSONDiffLine" -}}
<p id="l{{ .LineNum }}" class="diff-line diff-json-{{ cton .Change }}">
<span class="diff-line-number">
{{if .LeftLine }}{{ .LeftLine }}{{ end }}
</span>
<span class="diff-line-number">
{{if .RightLine }}{{ .RightLine }}{{ end }}
</span>
<span class="diff-value diff-indent-{{ .Indent }}" title="{{ .Text }}">
{{ .Text }}
</span>
<span class="diff-line-icon">{{ ctos .Change }}</span>
</p>
{{ end }}`
)
var diffTplFuncs = template.FuncMap{
"ctos": func(c ChangeType) string {
if symbol, ok := changeTypeToSymbol[c]; ok {
return symbol
}
return ""
},
"cton": func(c ChangeType) string {
if name, ok := changeTypeToName[c]; ok {
return name
}
return ""
},
}
// JSONLine contains the data required to render each line of the JSON diff
// and contains the data required to produce the tokens output in the basic
// diff.
type JSONLine struct {
LineNum int `json:"line"`
LeftLine int `json:"leftLine"`
RightLine int `json:"rightLine"`
Indent int `json:"indent"`
Text string `json:"text"`
Change ChangeType `json:"changeType"`
Key string `json:"key"`
Val interface{} `json:"value"`
}
func NewJSONFormatter(left interface{}) *JSONFormatter {
tpl := template.Must(template.New("JSONDiffWrapper").Funcs(diffTplFuncs).Parse(tplJSONDiffWrapper))
tpl = template.Must(tpl.New("JSONDiffLine").Funcs(diffTplFuncs).Parse(tplJSONDiffLine))
return &JSONFormatter{
left: left,
Lines: []*JSONLine{},
tpl: tpl,
path: []string{},
size: []int{},
lineCount: 0,
inArray: []bool{},
}
}
type JSONFormatter struct {
left interface{}
path []string
size []int
inArray []bool
lineCount int
leftLine int
rightLine int
line *AsciiLine
Lines []*JSONLine
tpl *template.Template
}
type AsciiLine struct {
// the type of change
change ChangeType
// the actual changes - no formatting
key string
val interface{}
// level of indentation for the current line
indent int
// buffer containing the fully formatted line
buffer *bytes.Buffer
}
func (f *JSONFormatter) Format(diff diff.Diff) (result string, err error) {
if v, ok := f.left.(map[string]interface{}); ok {
f.formatObject(v, diff)
} else if v, ok := f.left.([]interface{}); ok {
f.formatArray(v, diff)
} else {
return "", fmt.Errorf("expected map[string]interface{} or []interface{}, got %T",
f.left)
}
b := &bytes.Buffer{}
err = f.tpl.ExecuteTemplate(b, "JSONDiffWrapper", f.Lines)
if err != nil {
fmt.Printf("%v\n", err)
return "", err
}
return b.String(), nil
}
func (f *JSONFormatter) formatObject(left map[string]interface{}, df diff.Diff) {
f.addLineWith(ChangeNil, "{")
f.push("ROOT", len(left), false)
f.processObject(left, df.Deltas())
f.pop()
f.addLineWith(ChangeNil, "}")
}
func (f *JSONFormatter) formatArray(left []interface{}, df diff.Diff) {
f.addLineWith(ChangeNil, "[")
f.push("ROOT", len(left), true)
f.processArray(left, df.Deltas())
f.pop()
f.addLineWith(ChangeNil, "]")
}
func (f *JSONFormatter) processArray(array []interface{}, deltas []diff.Delta) error {
patchedIndex := 0
for index, value := range array {
f.processItem(value, deltas, diff.Index(index))
patchedIndex++
}
// additional Added
for _, delta := range deltas {
switch delta.(type) {
case *diff.Added:
d := delta.(*diff.Added)
// skip items already processed
if int(d.Position.(diff.Index)) < len(array) {
continue
}
f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
}
}
return nil
}
func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []diff.Delta) error {
names := sortKeys(object)
for _, name := range names {
value := object[name]
f.processItem(value, deltas, diff.Name(name))
}
// Added
for _, delta := range deltas {
switch delta.(type) {
case *diff.Added:
d := delta.(*diff.Added)
f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
}
}
return nil
}
func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, position diff.Position) error {
matchedDeltas := f.searchDeltas(deltas, position)
positionStr := position.String()
if len(matchedDeltas) > 0 {
for _, matchedDelta := range matchedDeltas {
switch matchedDelta.(type) {
case *diff.Object:
d := matchedDelta.(*diff.Object)
switch value.(type) {
case map[string]interface{}:
//ok
default:
return errors.New("Type mismatch")
}
o := value.(map[string]interface{})
f.newLine(ChangeNil)
f.printKey(positionStr)
f.print("{")
f.closeLine()
f.push(positionStr, len(o), false)
f.processObject(o, d.Deltas)
f.pop()
f.newLine(ChangeNil)
f.print("}")
f.printComma()
f.closeLine()
case *diff.Array:
d := matchedDelta.(*diff.Array)
switch value.(type) {
case []interface{}:
//ok
default:
return errors.New("Type mismatch")
}
a := value.([]interface{})
f.newLine(ChangeNil)
f.printKey(positionStr)
f.print("[")
f.closeLine()
f.push(positionStr, len(a), true)
f.processArray(a, d.Deltas)
f.pop()
f.newLine(ChangeNil)
f.print("]")
f.printComma()
f.closeLine()
case *diff.Added:
d := matchedDelta.(*diff.Added)
f.printRecursive(positionStr, d.Value, ChangeAdded)
f.size[len(f.size)-1]++
case *diff.Modified:
d := matchedDelta.(*diff.Modified)
savedSize := f.size[len(f.size)-1]
f.printRecursive(positionStr, d.OldValue, ChangeOld)
f.size[len(f.size)-1] = savedSize
f.printRecursive(positionStr, d.NewValue, ChangeNew)
case *diff.TextDiff:
savedSize := f.size[len(f.size)-1]
d := matchedDelta.(*diff.TextDiff)
f.printRecursive(positionStr, d.OldValue, ChangeOld)
f.size[len(f.size)-1] = savedSize
f.printRecursive(positionStr, d.NewValue, ChangeNew)
case *diff.Deleted:
d := matchedDelta.(*diff.Deleted)
f.printRecursive(positionStr, d.Value, ChangeDeleted)
default:
return errors.New("Unknown Delta type detected")
}
}
} else {
f.printRecursive(positionStr, value, ChangeUnchanged)
}
return nil
}
func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
results = make([]diff.Delta, 0)
for _, delta := range deltas {
switch delta.(type) {
case diff.PostDelta:
if delta.(diff.PostDelta).PostPosition() == position {
results = append(results, delta)
}
case diff.PreDelta:
if delta.(diff.PreDelta).PrePosition() == position {
results = append(results, delta)
}
default:
panic("heh")
}
}
return
}
func (f *JSONFormatter) push(name string, size int, array bool) {
f.path = append(f.path, name)
f.size = append(f.size, size)
f.inArray = append(f.inArray, array)
}
func (f *JSONFormatter) pop() {
f.path = f.path[0 : len(f.path)-1]
f.size = f.size[0 : len(f.size)-1]
f.inArray = f.inArray[0 : len(f.inArray)-1]
}
func (f *JSONFormatter) addLineWith(change ChangeType, value string) {
f.line = &AsciiLine{
change: change,
indent: len(f.path),
buffer: bytes.NewBufferString(value),
}
f.closeLine()
}
func (f *JSONFormatter) newLine(change ChangeType) {
f.line = &AsciiLine{
change: change,
indent: len(f.path),
buffer: bytes.NewBuffer([]byte{}),
}
}
func (f *JSONFormatter) closeLine() {
leftLine := 0
rightLine := 0
f.lineCount++
switch f.line.change {
case ChangeAdded, ChangeNew:
f.rightLine++
rightLine = f.rightLine
case ChangeDeleted, ChangeOld:
f.leftLine++
leftLine = f.leftLine
case ChangeNil, ChangeUnchanged:
f.rightLine++
f.leftLine++
rightLine = f.rightLine
leftLine = f.leftLine
}
s := f.line.buffer.String()
f.Lines = append(f.Lines, &JSONLine{
LineNum: f.lineCount,
RightLine: rightLine,
LeftLine: leftLine,
Indent: f.line.indent,
Text: s,
Change: f.line.change,
Key: f.line.key,
Val: f.line.val,
})
}
func (f *JSONFormatter) printKey(name string) {
if !f.inArray[len(f.inArray)-1] {
f.line.key = name
fmt.Fprintf(f.line.buffer, `"%s": `, name)
}
}
func (f *JSONFormatter) printComma() {
f.size[len(f.size)-1]--
if f.size[len(f.size)-1] > 0 {
f.line.buffer.WriteRune(',')
}
}
func (f *JSONFormatter) printValue(value interface{}) {
switch value.(type) {
case string:
f.line.val = value
fmt.Fprintf(f.line.buffer, `"%s"`, value)
case nil:
f.line.val = "null"
f.line.buffer.WriteString("null")
default:
f.line.val = value
fmt.Fprintf(f.line.buffer, `%#v`, value)
}
}
func (f *JSONFormatter) print(a string) {
f.line.buffer.WriteString(a)
}
func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
switch value.(type) {
case map[string]interface{}:
f.newLine(change)
f.printKey(name)
f.print("{")
f.closeLine()
m := value.(map[string]interface{})
size := len(m)
f.push(name, size, false)
keys := sortKeys(m)
for _, key := range keys {
f.printRecursive(key, m[key], change)
}
f.pop()
f.newLine(change)
f.print("}")
f.printComma()
f.closeLine()
case []interface{}:
f.newLine(change)
f.printKey(name)
f.print("[")
f.closeLine()
s := value.([]interface{})
size := len(s)
f.push("", size, true)
for _, item := range s {
f.printRecursive("", item, change)
}
f.pop()
f.newLine(change)
f.print("]")
f.printComma()
f.closeLine()
default:
f.newLine(change)
f.printKey(name)
f.printValue(value)
f.printComma()
f.closeLine()
}
}
func sortKeys(m map[string]interface{}) (keys []string) {
keys = make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return
}
+131
View File
@@ -0,0 +1,131 @@
package dashdiffs
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func TestDiff(t *testing.T) {
// Sample json docs for tests only
const (
leftJSON = `{
"key": "value",
"object": {
"key": "value",
"anotherObject": {
"same": "this field is the same in rightJSON",
"change": "this field should change in rightJSON",
"delete": "this field doesn't appear in rightJSON"
}
},
"array": [
"same",
"change",
"delete"
],
"embeddedArray": {
"array": [
"same",
"change",
"delete"
]
}
}`
rightJSON = `{
"key": "differentValue",
"object": {
"key": "value",
"newKey": "value",
"anotherObject": {
"same": "this field is the same in rightJSON",
"change": "this field should change in rightJSON",
"add": "this field is added"
}
},
"array": [
"same",
"changed!",
"add"
],
"embeddedArray": {
"array": [
"same",
"changed!",
"add"
]
}
}`
)
Convey("Testing dashboard diffs", t, func() {
// Compute the diff between the two JSON objects
baseData, err := simplejson.NewJson([]byte(leftJSON))
So(err, ShouldBeNil)
newData, err := simplejson.NewJson([]byte(rightJSON))
So(err, ShouldBeNil)
left, jsonDiff, err := getDiff(baseData, newData)
So(err, ShouldBeNil)
Convey("The JSONFormatter should produce the expected JSON tokens", func() {
f := NewJSONFormatter(left)
_, err := f.Format(jsonDiff)
So(err, ShouldBeNil)
// Total up the change types. If the number of different change
// types is correct, it means that the diff is producing correct
// output to the template rendered.
changeCounts := make(map[ChangeType]int)
for _, line := range f.Lines {
changeCounts[line.Change]++
}
// The expectedChangeCounts here were determined by manually
// looking at the JSON
expectedChangeCounts := map[ChangeType]int{
ChangeNil: 12,
ChangeAdded: 2,
ChangeDeleted: 1,
ChangeOld: 5,
ChangeNew: 5,
ChangeUnchanged: 5,
}
So(changeCounts, ShouldResemble, expectedChangeCounts)
})
Convey("The BasicFormatter should produce the expected BasicBlocks", func() {
f := NewBasicFormatter(left)
_, err := f.Format(jsonDiff)
So(err, ShouldBeNil)
bd := &BasicDiff{}
blocks := bd.Basic(f.jsonDiff.Lines)
changeCounts := make(map[ChangeType]int)
for _, block := range blocks {
for _, change := range block.Changes {
changeCounts[change.Change]++
}
for _, summary := range block.Summaries {
changeCounts[summary.Change]++
}
changeCounts[block.Change]++
}
expectedChangeCounts := map[ChangeType]int{
ChangeNil: 3,
ChangeAdded: 2,
ChangeDeleted: 1,
ChangeOld: 3,
}
So(changeCounts, ShouldResemble, expectedChangeCounts)
})
})
}
@@ -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")
}
@@ -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, "")
})
}
+92
View File
@@ -0,0 +1,92 @@
package imguploader
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
"golang.org/x/oauth2/google"
)
const (
tokenUrl string = "https://www.googleapis.com/auth/devstorage.read_write"
uploadUrl string = "https://www.googleapis.com/upload/storage/v1/b/%s/o?uploadType=media&name=%s&predefinedAcl=publicRead"
)
type GCSUploader struct {
keyFile string
bucket string
path string
log log.Logger
}
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) {
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)
if err != nil {
return "", err
}
u.log.Debug("Creating JWT conf")
conf, err := google.JWTConfigFromJSON(data, tokenUrl)
if err != nil {
return "", err
}
u.log.Debug("Creating HTTP client")
client := conf.Client(ctx)
err = u.uploadFile(client, imageDiskPath, key)
if err != nil {
return "", err
}
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", u.bucket, key), nil
}
func (u *GCSUploader) uploadFile(client *http.Client, imageDiskPath, key string) error {
u.log.Debug("Opening image file ", imageDiskPath)
fileReader, err := os.Open(imageDiskPath)
if err != nil {
return err
}
reqUrl := fmt.Sprintf(uploadUrl, u.bucket, key)
u.log.Debug("Request URL: ", reqUrl)
req, err := http.NewRequest("POST", reqUrl, fileReader)
if err != nil {
return err
}
req.Header.Add("Content-Type", "image/png")
u.log.Debug("Sending POST request to GCS")
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("GCS response status code %d", resp.StatusCode)
}
return nil
}
@@ -0,0 +1,24 @@
package imguploader
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestUploadToGCS(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.gcs", t, func() {
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
gcsUploader, _ := NewImageUploader()
path, err := gcsUploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
So(err, ShouldBeNil)
So(path, ShouldNotEqual, "")
})
}
+50 -7
View File
@@ -1,20 +1,22 @@
package imguploader
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/log"
"regexp"
"github.com/grafana/grafana/pkg/setting"
)
type ImageUploader interface {
Upload(path string) (string, error)
Upload(ctx context.Context, path string) (string, error)
}
type NopImageUploader struct {
}
func (NopImageUploader) Upload(path string) (string, error) {
func (NopImageUploader) Upload(ctx context.Context, path string) (string, error) {
return "", nil
}
@@ -27,15 +29,27 @@ func NewImageUploader() (ImageUploader, error) {
return nil, err
}
bucket := s3sec.Key("bucket").MustString("")
region := s3sec.Key("region").MustString("")
path := s3sec.Key("path").MustString("")
bucketUrl := s3sec.Key("bucket_url").MustString("")
accessKey := s3sec.Key("access_key").MustString("")
secretKey := s3sec.Key("secret_key").MustString("")
info, err := getRegionAndBucketFromUrl(bucketUrl)
if err != nil {
return nil, err
if path != "" && path[len(path)-1:] != "/" {
path += "/"
}
return NewS3Uploader(info.region, info.bucket, "public-read", accessKey, secretKey), nil
if bucket == "" || region == "" {
info, err := getRegionAndBucketFromUrl(bucketUrl)
if err != nil {
return nil, err
}
bucket = info.bucket
region = info.region
}
return NewS3Uploader(region, bucket, path, "public-read", accessKey, secretKey), nil
case "webdav":
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
if err != nil {
@@ -47,10 +61,39 @@ func NewImageUploader() (ImageUploader, error) {
return nil, fmt.Errorf("Could not find url key for image.uploader.webdav")
}
public_url := webdavSec.Key("public_url").String()
username := webdavSec.Key("username").String()
password := webdavSec.Key("password").String()
return NewWebdavImageUploader(url, username, password)
return NewWebdavImageUploader(url, username, password, public_url)
case "gcs":
gcssec, err := setting.Cfg.GetSection("external_image_storage.gcs")
if err != nil {
return nil, err
}
keyFile := gcssec.Key("key_file").MustString("")
bucketName := gcssec.Key("bucket").MustString("")
path := gcssec.Key("path").MustString("")
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
@@ -96,5 +96,70 @@ func TestImageUploaderFactory(t *testing.T) {
So(original.username, ShouldEqual, "username")
So(original.password, ShouldEqual, "password")
})
Convey("GCS uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "gcs"
gcpSec, err := setting.Cfg.GetSection("external_image_storage.gcs")
gcpSec.NewKey("key_file", "/etc/secrets/project-79a52befa3f6.json")
gcpSec.NewKey("bucket", "project-grafana-east")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*GCSUploader)
So(ok, ShouldBeTrue)
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)
})
})
}
@@ -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
}
@@ -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")
})
}
+11 -6
View File
@@ -1,6 +1,7 @@
package imguploader
import (
"context"
"os"
"time"
@@ -8,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/grafana/grafana/pkg/log"
@@ -17,16 +19,18 @@ import (
type S3Uploader struct {
region string
bucket string
path string
acl string
secretKey string
accessKey string
log log.Logger
}
func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader {
func NewS3Uploader(region, bucket, path, acl, accessKey, secretKey string) *S3Uploader {
return &S3Uploader{
region: region,
bucket: bucket,
path: path,
acl: acl,
accessKey: accessKey,
secretKey: secretKey,
@@ -34,7 +38,7 @@ func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader
}
}
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
sess, err := session.NewSession()
if err != nil {
return "", err
@@ -53,8 +57,10 @@ func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
Credentials: creds,
}
key := util.GetRandomString(20) + ".png"
log.Debug("Uploading image to s3", "bucket = ", u.bucket, ", key = ", key)
s3_endpoint, _ := endpoints.DefaultResolver().EndpointFor("s3", u.region)
key := u.path + util.GetRandomString(20) + ".png"
image_url := s3_endpoint.URL + "/" + u.bucket + "/" + key
log.Debug("Uploading image to s3", "url = ", image_url)
file, err := os.Open(imageDiskPath)
if err != nil {
@@ -77,6 +83,5 @@ func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
if err != nil {
return "", err
}
return "https://" + u.bucket + ".s3.amazonaws.com/" + key, nil
return image_url, nil
}
@@ -1,6 +1,7 @@
package imguploader
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/setting"
@@ -8,14 +9,14 @@ import (
)
func TestUploadToS3(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.webdav", t, func() {
SkipConvey("[Integration test] for external_image_store.s3", t, func() {
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
s3Uploader, _ := NewImageUploader()
path, err := s3Uploader.Upload("../../../public/img/logo_transparent_400x.png")
path, err := s3Uploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
So(err, ShouldBeNil)
So(path, ShouldNotEqual, "")
+22 -10
View File
@@ -2,6 +2,7 @@ package imguploader
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net"
@@ -14,14 +15,17 @@ import (
)
type WebdavUploader struct {
url string
username string
password string
url string
username string
password string
public_url string
}
var netTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 60 * time.Second,
Timeout: 60 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
@@ -31,9 +35,10 @@ var netClient = &http.Client{
Transport: netTransport,
}
func (u *WebdavUploader) Upload(pa string) (string, error) {
func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
url, _ := url.Parse(u.url)
url.Path = path.Join(url.Path, util.GetRandomString(20)+".png")
filename := util.GetRandomString(20) + ".png"
url.Path = path.Join(url.Path, filename)
imgData, err := ioutil.ReadFile(pa)
req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData))
@@ -53,13 +58,20 @@ func (u *WebdavUploader) Upload(pa string) (string, error) {
return "", fmt.Errorf("Failed to upload image. Returned statuscode %v body %s", res.StatusCode, body)
}
if u.public_url != "" {
publicURL, _ := url.Parse(u.public_url)
publicURL.Path = path.Join(publicURL.Path, filename)
return publicURL.String(), nil
}
return url.String(), nil
}
func NewWebdavImageUploader(url, username, passwrod string) (*WebdavUploader, error) {
func NewWebdavImageUploader(url, username, password, public_url string) (*WebdavUploader, error) {
return &WebdavUploader{
url: url,
username: username,
password: passwrod,
url: url,
username: username,
password: password,
public_url: public_url,
}, nil
}
@@ -1,18 +1,28 @@
package imguploader
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestUploadToWebdav(t *testing.T) {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:9998/dav/", "username", "password")
// Can be tested with this docker container: https://hub.docker.com/r/morrisjobke/webdav/
SkipConvey("[Integration test] for external_image_store.webdav", t, func() {
path, err := webdavUploader.Upload("../../../public/img/logo_transparent_400x.png")
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "")
path, err := webdavUploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
So(err, ShouldBeNil)
So(path, ShouldNotEqual, "")
So(path, ShouldStartWith, "http://localhost:8888/webdav/")
})
SkipConvey("[Integration test] for external_image_store.webdav with public url", t, func() {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://publicurl:8888/webdav")
path, err := webdavUploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
So(err, ShouldBeNil)
So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
})
}
+47 -22
View File
@@ -1,6 +1,7 @@
package renderer
import (
"errors"
"fmt"
"io"
"os"
@@ -15,19 +16,25 @@ 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")
var rendererLog log.Logger = log.New("png-renderer")
func isoTimeOffsetToPosixTz(isoOffset string) string {
@@ -65,38 +72,58 @@ 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)
if err != nil {
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,
"height=" + params.Height,
"png=" + pngPath,
"domain=" + localDomain,
"timeout=" + strconv.Itoa(timeout),
"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()
@@ -105,29 +132,27 @@ 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)
}()
timeout, err := strconv.Atoi(params.Timeout)
if err != nil {
timeout = 15
}
select {
case <-time.After(time.Duration(timeout) * time.Second):
if err := cmd.Process.Kill(); err != nil {
rendererLog.Error("failed to kill", "error", err)
}
return "", fmt.Errorf("PhantomRenderer::renderToPng timeout (>%vs)", timeout)
return "", ErrTimeout
case <-done:
}
@@ -1,6 +1,7 @@
package securejsondata
import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -10,7 +11,12 @@ type SecureJsonData map[string][]byte
func (s SecureJsonData) Decrypt() map[string]string {
decrypted := make(map[string]string)
for key, data := range s {
decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
decryptedData, err := util.Decrypt(data, setting.SecretKey)
if err != nil {
log.Fatal(4, err.Error())
}
decrypted[key] = string(decryptedData)
}
return decrypted
}
@@ -18,7 +24,12 @@ func (s SecureJsonData) Decrypt() map[string]string {
func GetEncryptedJsonData(sjd map[string]string) SecureJsonData {
encrypted := make(SecureJsonData)
for key, data := range sjd {
encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey)
encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey)
if err != nil {
log.Fatal(4, err.Error())
}
encrypted[key] = encryptedData
}
return encrypted
}
+28 -3
View File
@@ -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
View 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)
})
}
+37 -15
View File
@@ -14,11 +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)
@@ -34,7 +37,7 @@ func New(logger string, ctx ...interface{}) Logger {
func Trace(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@@ -45,7 +48,7 @@ func Trace(format string, v ...interface{}) {
func Debug(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@@ -60,7 +63,7 @@ func Debug2(message string, v ...interface{}) {
func Info(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@@ -75,7 +78,7 @@ func Info2(message string, v ...interface{}) {
func Warn(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@@ -88,7 +91,7 @@ func Warn2(message string, v ...interface{}) {
}
func Error(skip int, format string, v ...interface{}) {
Root.Error(fmt.Sprintf(format, v))
Root.Error(fmt.Sprintf(format, v...))
}
func Error2(message string, v ...interface{}) {
@@ -96,7 +99,7 @@ func Error2(message string, v ...interface{}) {
}
func Critical(skip int, format string, v ...interface{}) {
Root.Crit(fmt.Sprintf(format, v))
Root.Crit(fmt.Sprintf(format, v...))
}
func Fatal(skip int, format string, v ...interface{}) {
@@ -112,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,
@@ -155,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()
@@ -172,7 +194,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
Close()
defaultLevelName, _ := getLogLevelFromConfig("log", "info", cfg)
defaultFilters := getFilters(cfg.Section("log").Key("filters").Strings(" "))
defaultFilters := getFilters(util.SplitString(cfg.Section("log").Key("filters").String()))
handlers := make([]log15.Handler, 0)
@@ -185,7 +207,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
// Log level.
_, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg)
modeFilters := getFilters(sec.Key("filters").Strings(" "))
filters := getFilters(util.SplitString(sec.Key("filters").String()))
format := getLogFormat(sec.Key("format").MustString(""))
var handler log15.Handler
@@ -217,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)
}
@@ -234,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
View File
@@ -0,0 +1,39 @@
package log
import (
"io"
"strings"
)
type logWriterImpl struct {
log Logger
level Lvl
prefix string
}
func NewLogWriter(log Logger, level Lvl, prefix string) io.Writer {
return &logWriterImpl{
log: log,
level: level,
prefix: prefix,
}
}
func (l *logWriterImpl) Write(p []byte) (n int, err error) {
message := l.prefix + strings.TrimSpace(string(p))
switch l.level {
case LvlCrit:
l.log.Crit(message)
case LvlError:
l.log.Error(message)
case LvlWarn:
l.log.Warn(message)
case LvlInfo:
l.log.Info(message)
default:
l.log.Debug(message)
}
return len(p), nil
}

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