Merge branch 'master' into add_google_hangouts_chat_notifier

This commit is contained in:
Patrick
2018-11-18 22:00:53 +01:00
committed by GitHub
2372 changed files with 186632 additions and 66461 deletions

View File

@@ -12,7 +12,7 @@ import (
func AdminGetSettings(c *m.ReqContext) {
settings := make(map[string]interface{})
for _, section := range setting.Cfg.Sections() {
for _, section := range setting.Raw.Sections() {
jsonSec := make(map[string]interface{})
settings[section.Name()] = jsonSec

View File

@@ -47,14 +47,14 @@ func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) {
}
func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) {
userId := c.ParamsInt64(":id")
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)
@@ -64,7 +64,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt)
cmd := m.ChangeUserPasswordCommand{
UserId: userId,
UserId: userID,
NewPassword: passwordHashed,
}
@@ -77,10 +77,10 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
}
func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
userId := c.ParamsInt64(":id")
userID := c.ParamsInt64(":id")
cmd := m.UpdateUserPermissionsCommand{
UserId: userId,
UserId: userID,
IsGrafanaAdmin: form.IsGrafanaAdmin,
}
@@ -93,9 +93,9 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis
}
func AdminDeleteUser(c *m.ReqContext) {
userId := c.ParamsInt64(":id")
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)

View File

@@ -2,12 +2,14 @@ package api
import (
"fmt"
"strconv"
"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/alerting"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/search"
)
func ValidateOrgAlert(c *m.ReqContext) {
@@ -26,10 +28,10 @@ func ValidateOrgAlert(c *m.ReqContext) {
}
func GetAlertStatesForDashboard(c *m.ReqContext) Response {
dashboardId := c.QueryInt64("dashboardId")
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 := m.GetAlertStatesForDashboardQuery{
@@ -38,20 +40,72 @@ func GetAlertStatesForDashboard(c *m.ReqContext) Response {
}
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 *m.ReqContext) Response {
dashboardQuery := c.Query("dashboardQuery")
dashboardTags := c.QueryStrings("dashboardTag")
stringDashboardIDs := c.QueryStrings("dashboardId")
stringFolderIDs := c.QueryStrings("folderId")
dashboardIDs := make([]int64, 0)
for _, id := range stringDashboardIDs {
dashboardID, err := strconv.ParseInt(id, 10, 64)
if err == nil {
dashboardIDs = append(dashboardIDs, dashboardID)
}
}
if dashboardQuery != "" || len(dashboardTags) > 0 || len(stringFolderIDs) > 0 {
folderIDs := make([]int64, 0)
for _, id := range stringFolderIDs {
folderID, err := strconv.ParseInt(id, 10, 64)
if err == nil {
folderIDs = append(folderIDs, folderID)
}
}
searchQuery := search.Query{
Title: dashboardQuery,
Tags: dashboardTags,
SignedInUser: c.SignedInUser,
Limit: 1000,
OrgId: c.OrgId,
DashboardIds: dashboardIDs,
Type: string(search.DashHitDB),
FolderIds: folderIDs,
Permission: m.PERMISSION_VIEW,
}
err := bus.Dispatch(&searchQuery)
if err != nil {
return Error(500, "List alerts failed", err)
}
for _, d := range searchQuery.Result {
if d.Type == search.DashHitDB && d.Id > 0 {
dashboardIDs = append(dashboardIDs, d.Id)
}
}
// if we didn't find any dashboards, return empty result
if len(dashboardIDs) == 0 {
return JSON(200, []*m.AlertListItemDTO{})
}
}
query := m.GetAlertsQuery{
OrgId: c.OrgId,
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"),
User: c.SignedInUser,
OrgId: c.OrgId,
DashboardIDs: dashboardIDs,
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"),
User: c.SignedInUser,
Query: c.Query("query"),
}
states := c.QueryStrings("state")
@@ -60,33 +114,37 @@ func GetAlerts(c *m.ReqContext) Response {
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "List alerts failed", err)
return Error(500, "List alerts failed", err)
}
for _, alert := range query.Result {
alert.Url = m.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
}
return Json(200, query.Result)
return JSON(200, query.Result)
}
// POST /api/alerts/test
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{
OrgId: c.OrgId,
Dashboard: dto.Dashboard,
PanelId: dto.PanelId,
User: c.SignedInUser,
}
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)
if err == m.ErrDataSourceAccessDenied {
return Error(403, "Access denied to datasource", err)
}
return Error(500, "Failed to test rule", err)
}
res := backendCmd.Result
@@ -109,7 +167,7 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs())
return Json(200, dtoRes)
return JSON(200, dtoRes)
}
// GET /api/alerts/:id
@@ -118,70 +176,63 @@ func GetAlert(c *m.ReqContext) Response {
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)
}
func GetAlertNotifiers(c *m.ReqContext) Response {
return Json(200, alerting.GetNotifiers())
return JSON(200, alerting.GetNotifiers())
}
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)
for _, notification := range query.Result {
result = append(result, &dtos.AlertNotification{
Id: notification.Id,
Name: notification.Name,
Type: notification.Type,
IsDefault: notification.IsDefault,
Created: notification.Created,
Updated: notification.Updated,
})
result = append(result, dtos.NewAlertNotification(notification))
}
return Json(200, result)
return JSON(200, result)
}
func GetAlertNotificationById(c *m.ReqContext) Response {
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, dtos.NewAlertNotification(query.Result))
}
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, dtos.NewAlertNotification(cmd.Result))
}
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, dtos.NewAlertNotification(cmd.Result))
}
func DeleteAlertNotification(c *m.ReqContext) Response {
@@ -191,10 +242,10 @@ func DeleteAlertNotification(c *m.ReqContext) Response {
}
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
@@ -207,41 +258,41 @@ func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Respons
if err := bus.Dispatch(cmd); err != nil {
if err == m.ErrSmtpNotEnabled {
return ApiError(412, err.Error(), err)
return Error(412, err.Error(), err)
}
return ApiError(500, "Failed to send alert notifications", 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 *m.ReqContext, dto dtos.PauseAlertCommand) Response {
alertId := c.ParamsInt64("alertId")
alertID := c.ParamsInt64("alertId")
query := m.GetAlertByIdQuery{Id: alertId}
query := m.GetAlertByIdQuery{Id: alertID}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Get Alert failed", err)
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 ApiError(500, "Error while checking permissions for Alert", err)
return Error(500, "Error while checking permissions for Alert", err)
}
return ApiError(403, "Access denied to this dashboard and alert", nil)
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 m.AlertStateType = m.AlertStatePending
@@ -252,12 +303,12 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
}
result := map[string]interface{}{
"alertId": alertId,
"alertId": alertID,
"state": response,
"message": "Alert " + pausedState,
}
return Json(200, result)
return JSON(200, result)
}
//POST /api/admin/pause-all-alerts
@@ -267,7 +318,7 @@ func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
}
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 m.AlertStateType = m.AlertStatePending
@@ -283,5 +334,5 @@ func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
"alertsAffected": updateCmd.ResultCount,
}
return Json(200, result)
return JSON(200, result)
}

View File

@@ -6,6 +6,7 @@ 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/services/search"
. "github.com/smartystreets/goconvey/convey"
)
@@ -30,7 +31,7 @@ func TestAlertingApiEndpoint(t *testing.T) {
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
query.Result = []*m.TeamDTO{}
return nil
})
@@ -64,6 +65,60 @@ func TestAlertingApiEndpoint(t *testing.T) {
})
})
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) {
var searchQuery *search.Query
bus.AddHandler("test", func(query *search.Query) error {
searchQuery = query
return nil
})
var getAlertsQuery *m.GetAlertsQuery
bus.AddHandler("test", func(query *m.GetAlertsQuery) error {
getAlertsQuery = query
return nil
})
sc.handlerFunc = GetAlerts
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(searchQuery, ShouldBeNil)
So(getAlertsQuery, ShouldNotBeNil)
})
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) {
var searchQuery *search.Query
bus.AddHandler("test", func(query *search.Query) error {
searchQuery = query
query.Result = search.HitList{
&search.Hit{Id: 1},
&search.Hit{Id: 2},
}
return nil
})
var getAlertsQuery *m.GetAlertsQuery
bus.AddHandler("test", func(query *m.GetAlertsQuery) error {
getAlertsQuery = query
return nil
})
sc.handlerFunc = GetAlerts
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(searchQuery, ShouldNotBeNil)
So(searchQuery.DashboardIds[0], ShouldEqual, 1)
So(searchQuery.DashboardIds[1], ShouldEqual, 2)
So(searchQuery.FolderIds[0], ShouldEqual, 3)
So(searchQuery.Tags[0], ShouldEqual, "abc")
So(searchQuery.Title, ShouldEqual, "dbQuery")
So(getAlertsQuery, ShouldNotBeNil)
So(getAlertsQuery.DashboardIDs[0], ShouldEqual, 1)
So(getAlertsQuery.DashboardIDs[1], ShouldEqual, 2)
So(getAlertsQuery.Limit, ShouldEqual, 5)
So(getAlertsQuery.Query, ShouldEqual, "alertQuery")
})
})
}
@@ -80,7 +135,7 @@ func postAlertScenario(desc string, url string, routePattern string, role m.Role
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID

View File

@@ -2,7 +2,6 @@ package api
import (
"strings"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -15,32 +14,33 @@ import (
func GetAnnotations(c *m.ReqContext) Response {
query := &annotations.ItemQuery{
From: c.QueryInt64("from") / 1000,
To: c.QueryInt64("to") / 1000,
From: c.QueryInt64("from"),
To: c.QueryInt64("to"),
OrgId: c.OrgId,
UserId: c.QueryInt64("userId"),
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"),
Tags: c.QueryStrings("tags"),
Type: c.Query("type"),
MatchAny: c.QueryBool("matchAny"),
}
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)
}
for _, item := range items {
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
item.Time = item.Time * 1000
}
return Json(200, items)
return JSON(200, items)
}
type CreateAnnotationError struct {
@@ -52,7 +52,7 @@ func (e *CreateAnnotationError) Error() string {
}
func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
if canSave, err := canSaveByDashboardId(c, cmd.DashboardId); err != nil || !canSave {
if canSave, err := canSaveByDashboardID(c, cmd.DashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -60,7 +60,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
if cmd.Text == "" {
err := &CreateAnnotationError{"text field should not be empty"}
return ApiError(500, "Failed to save annotation", err)
return Error(500, "Failed to save annotation", err)
}
item := annotations.Item{
@@ -68,18 +68,14 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
UserId: c.UserId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
Epoch: cmd.Time / 1000,
Epoch: cmd.Time,
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 ApiError(500, "Failed to save annotation", err)
return Error(500, "Failed to save annotation", err)
}
startID := item.Id
@@ -93,24 +89,24 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
}
if err := repo.Update(&item); err != nil {
return ApiError(500, "Failed set regionId on annotation", err)
return Error(500, "Failed set regionId on annotation", err)
}
item.Id = 0
item.Epoch = cmd.TimeEnd / 1000
item.Epoch = cmd.TimeEnd
if err := repo.Save(&item); err != nil {
return ApiError(500, "Failed save annotation for region end time", err)
return Error(500, "Failed save annotation for region end time", err)
}
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"message": "Annotation added",
"id": startID,
"endId": item.Id,
})
}
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"message": "Annotation added",
"id": startID,
})
@@ -129,12 +125,9 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
if cmd.What == "" {
err := &CreateAnnotationError{"what field should not be empty"}
return ApiError(500, "Failed to save Graphite annotation", err)
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)
@@ -152,133 +145,137 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
tagsArray = append(tagsArray, tagStr)
} else {
err := &CreateAnnotationError{"tag should be a string"}
return ApiError(500, "Failed to save Graphite annotation", err)
return Error(500, "Failed to save Graphite annotation", err)
}
}
default:
err := &CreateAnnotationError{"unsupported tags format"}
return ApiError(500, "Failed to save Graphite annotation", err)
return Error(500, "Failed to save Graphite annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Epoch: cmd.When,
Epoch: cmd.When * 1000,
Text: text,
Tags: tagsArray,
}
if err := repo.Save(&item); err != nil {
return ApiError(500, "Failed to save Graphite annotation", err)
return Error(500, "Failed to save Graphite annotation", err)
}
return Json(200, util.DynMap{
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")
annotationID := c.ParamsInt64(":annotationId")
repo := annotations.GetRepository()
if resp := canSave(c, repo, annotationId); resp != nil {
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,
Id: annotationID,
Epoch: cmd.Time,
Text: cmd.Text,
Tags: cmd.Tags,
}
if err := repo.Update(&item); err != nil {
return ApiError(500, "Failed to update annotation", err)
return Error(500, "Failed to update annotation", err)
}
if cmd.IsRegion {
itemRight := item
itemRight.RegionId = item.Id
itemRight.Epoch = cmd.TimeEnd / 1000
itemRight.Epoch = cmd.TimeEnd
// 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 ApiError(500, "Failed to update annotation for region end time", err)
return Error(500, "Failed to update annotation for region end time", err)
}
}
return ApiSuccess("Annotation updated")
return Success("Annotation updated")
}
func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
repo := annotations.GetRepository()
err := repo.Delete(&annotations.DeleteParams{
AlertId: cmd.PanelId,
OrgId: c.OrgId,
Id: cmd.AnnotationId,
RegionId: cmd.RegionId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
})
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 {
func DeleteAnnotationByID(c *m.ReqContext) Response {
repo := annotations.GetRepository()
annotationId := c.ParamsInt64(":annotationId")
annotationID := c.ParamsInt64(":annotationId")
if resp := canSave(c, repo, annotationId); resp != nil {
if resp := canSave(c, repo, annotationID); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{
Id: annotationId,
OrgId: c.OrgId,
Id: annotationID,
})
if err != nil {
return ApiError(500, "Failed to delete annotation", err)
return Error(500, "Failed to delete annotation", err)
}
return ApiSuccess("Annotation deleted")
return Success("Annotation deleted")
}
func DeleteAnnotationRegion(c *m.ReqContext) Response {
repo := annotations.GetRepository()
regionId := c.ParamsInt64(":regionId")
regionID := c.ParamsInt64(":regionId")
if resp := canSave(c, repo, regionId); resp != nil {
if resp := canSave(c, repo, regionID); resp != nil {
return resp
}
err := repo.Delete(&annotations.DeleteParams{
RegionId: regionId,
OrgId: c.OrgId,
RegionId: regionID,
})
if err != nil {
return ApiError(500, "Failed to delete annotation region", err)
return Error(500, "Failed to delete annotation region", err)
}
return ApiSuccess("Annotation region deleted")
return Success("Annotation region deleted")
}
func canSaveByDashboardId(c *m.ReqContext, dashboardId int64) (bool, error) {
if dashboardId == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) {
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 {
if dashboardID != 0 {
guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err
}
}
@@ -286,32 +283,16 @@ func canSaveByDashboardId(c *m.ReqContext, dashboardId int64) (bool, error) {
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})
func canSave(c *m.ReqContext, repo annotations.Repository, annotationID int64) Response {
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return ApiError(500, "Could not find annotation to update", err)
return Error(500, "Could not find annotation to update", err)
}
dashboardId := items[0].DashboardId
dashboardID := items[0].DashboardId
if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
return nil
}
func canSaveByRegionId(c *m.ReqContext, repo annotations.Repository, regionId int64) Response {
items, err := repo.Find(&annotations.ItemQuery{RegionId: regionId, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return ApiError(500, "Could not find annotation to update", err)
}
dashboardId := items[0].DashboardId
if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave {
return dashboardGuardianResponse(err)
}

View File

@@ -41,7 +41,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
@@ -68,7 +68,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
@@ -100,6 +100,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
Id: 1,
}
deleteCmd := dtos.DeleteAnnotationsCmd{
DashboardId: 1,
PanelId: 1,
}
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
@@ -114,7 +119,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
query.Result = []*m.TeamDTO{}
return nil
})
@@ -132,7 +137,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
@@ -159,7 +164,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationById
sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
@@ -171,6 +176,25 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
})
})
})
Convey("When user is an Admin", func() {
role := m.ROLE_ADMIN
Convey("Should be able to do anything", func() {
postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
})
}
@@ -199,7 +223,7 @@ func postAnnotationScenario(desc string, url string, routePattern string, role m
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
@@ -222,7 +246,7 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m.
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
@@ -239,3 +263,26 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m.
fn(sc)
})
}
func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
return DeleteAnnotations(c, cmd)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -4,369 +4,379 @@ import (
"github.com/go-macaron/binding"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
// Register adds http routes
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()
func (hs *HTTPServer) registerRoutes() {
reqSignedIn := middleware.ReqSignedIn
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
reqEditorRole := middleware.ReqEditorRole
reqOrgAdmin := middleware.ReqOrgAdmin
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
quota := middleware.Quota
bind := binding.Bind
// automatically set HEAD for every GET
macaronR.SetAutoHead(true)
r := newRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)
r := hs.RouteRegister
// not logged in views
r.Get("/", reqSignedIn, Index)
r.Get("/", reqSignedIn, hs.Index)
r.Get("/logout", Logout)
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), wrap(LoginPost))
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
r.Get("/login/:name", quota("session"), OAuthLogin)
r.Get("/login", LoginView)
r.Get("/invite/:code", Index)
r.Get("/login", hs.LoginView)
r.Get("/invite/:code", hs.Index)
// authed views
r.Get("/profile/", reqSignedIn, Index)
r.Get("/profile/password", reqSignedIn, Index)
r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome)
r.Get("/org/", reqSignedIn, Index)
r.Get("/org/new", reqSignedIn, Index)
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/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)
r.Get("/admin/users/create", reqGrafanaAdmin, Index)
r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index)
r.Get("/admin/orgs", reqGrafanaAdmin, Index)
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
r.Get("/admin/stats", reqGrafanaAdmin, Index)
r.Get("/profile/", reqSignedIn, hs.Index)
r.Get("/profile/password", reqSignedIn, hs.Index)
r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome)
r.Get("/org/", reqSignedIn, hs.Index)
r.Get("/org/new", reqSignedIn, hs.Index)
r.Get("/datasources/", reqSignedIn, hs.Index)
r.Get("/datasources/new", reqSignedIn, hs.Index)
r.Get("/datasources/edit/*", reqSignedIn, hs.Index)
r.Get("/org/users", reqSignedIn, hs.Index)
r.Get("/org/users/new", reqSignedIn, hs.Index)
r.Get("/org/users/invite", reqSignedIn, hs.Index)
r.Get("/org/teams", reqSignedIn, hs.Index)
r.Get("/org/teams/*", reqSignedIn, hs.Index)
r.Get("/org/apikeys/", reqSignedIn, hs.Index)
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
r.Get("/admin", reqGrafanaAdmin, hs.Index)
r.Get("/admin/settings", reqGrafanaAdmin, hs.Index)
r.Get("/admin/users", reqGrafanaAdmin, hs.Index)
r.Get("/admin/users/create", reqGrafanaAdmin, hs.Index)
r.Get("/admin/users/edit/:id", reqGrafanaAdmin, hs.Index)
r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index)
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
r.Get("/admin/stats", reqGrafanaAdmin, hs.Index)
r.Get("/styleguide", reqSignedIn, Index)
r.Get("/styleguide", reqSignedIn, hs.Index)
r.Get("/plugins", reqSignedIn, Index)
r.Get("/plugins/:id/edit", reqSignedIn, Index)
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
r.Get("/plugins", reqSignedIn, hs.Index)
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index)
r.Get("/plugins/:id/page/:page", reqSignedIn, hs.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("/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("/d/:uid/:slug", reqSignedIn, hs.Index)
r.Get("/d/:uid", reqSignedIn, hs.Index)
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, hs.Index)
r.Get("/dashboard/script/*", reqSignedIn, hs.Index)
r.Get("/dashboard-solo/snapshot/*", hs.Index)
r.Get("/d-solo/:uid/:slug", reqSignedIn, hs.Index)
r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, hs.Index)
r.Get("/dashboard-solo/script/*", reqSignedIn, hs.Index)
r.Get("/import/dashboard", reqSignedIn, hs.Index)
r.Get("/dashboards/", reqSignedIn, hs.Index)
r.Get("/dashboards/*", reqSignedIn, hs.Index)
r.Get("/playlists/", reqSignedIn, Index)
r.Get("/playlists/*", reqSignedIn, Index)
r.Get("/alerting/", reqSignedIn, Index)
r.Get("/alerting/*", reqSignedIn, Index)
r.Get("/explore", reqEditorRole, hs.Index)
r.Get("/playlists/", reqSignedIn, hs.Index)
r.Get("/playlists/*", reqSignedIn, hs.Index)
r.Get("/alerting/", reqSignedIn, hs.Index)
r.Get("/alerting/*", reqSignedIn, hs.Index)
// sign up
r.Get("/signup", Index)
r.Get("/api/user/signup/options", wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), wrap(SignUp))
r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), wrap(SignUpStep2))
r.Get("/signup", hs.Index)
r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
// invited
r.Get("/api/user/invite/:code", wrap(GetInviteInfoByCode))
r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), wrap(CompleteInvite))
r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
// reset password
r.Get("/user/password/send-reset-email", Index)
r.Get("/user/password/reset", Index)
r.Get("/user/password/send-reset-email", hs.Index)
r.Get("/user/password/reset", hs.Index)
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail))
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), Wrap(SendResetPasswordEmail))
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), Wrap(ResetPassword))
// dashboard snapshots
r.Get("/dashboard/snapshot/*", Index)
r.Get("/dashboard/snapshots/", reqSignedIn, Index)
r.Get("/dashboard/snapshot/*", hs.Index)
r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
// api for dashboard snapshots
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, wrap(DeleteDashboardSnapshot))
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/: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(apiRoute RouteRegister) {
r.Group("/api", func(apiRoute routing.RouteRegister) {
// user (signed in)
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))
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
userRoute.Get("/", Wrap(GetSignedInUser))
userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
userRoute.Post("/stars/dashboard/:id", wrap(StarDashboard))
userRoute.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
userRoute.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
userRoute.Get("/quotas", wrap(GetUserQuotas))
userRoute.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
userRoute.Get("/helpflags/clear", wrap(ClearHelpFlags))
userRoute.Get("/helpflags/clear", Wrap(ClearHelpFlags))
userRoute.Get("/preferences", wrap(GetUserPreferences))
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
userRoute.Get("/preferences", Wrap(GetUserPreferences))
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
})
// users (admin permission required)
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))
apiRoute.Group("/users", func(usersRoute routing.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
usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail))
usersRoute.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
usersRoute.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))
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID))
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember))
teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences))
teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences))
}, reqOrgAdmin)
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", Wrap(GetTeamByID))
teamsRoute.Get("/search", Wrap(SearchTeams))
})
// org information available to all users.
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent))
orgRoute.Get("/quotas", wrap(GetOrgQuotas))
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/", Wrap(GetOrgCurrent))
orgRoute.Get("/quotas", Wrap(GetOrgQuotas))
})
// current org
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))
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
// invites
orgRoute.Get("/invites", wrap(GetPendingOrgInvites))
orgRoute.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
orgRoute.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
orgRoute.Get("/preferences", wrap(GetOrgPreferences))
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
orgRoute.Get("/preferences", Wrap(GetOrgPreferences))
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateOrgPreferences))
}, reqOrgAdmin)
// current org without requirement of user to be org admin
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/users", Wrap(GetOrgUsersForCurrentOrg))
})
// create new org
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), Wrap(CreateOrg))
// search all orgs
apiRoute.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
apiRoute.Get("/orgs", reqGrafanaAdmin, Wrap(SearchOrgs))
// orgs (admin routes)
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))
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
orgsRoute.Get("/", Wrap(GetOrgByID))
orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrg))
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddress))
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)
apiRoute.Group("/orgs/name/:name", func(orgsRoute RouteRegister) {
orgsRoute.Get("/", wrap(GetOrgByName))
apiRoute.Group("/orgs/name/:name", func(orgsRoute routing.RouteRegister) {
orgsRoute.Get("/", Wrap(GetOrgByName))
}, reqGrafanaAdmin)
// auth api keys
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))
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
keysRoute.Get("/", Wrap(GetAPIKeys))
keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), Wrap(AddAPIKey))
keysRoute.Delete("/:id", Wrap(DeleteAPIKey))
}, reqOrgAdmin)
// Preferences
apiRoute.Group("/preferences", func(prefRoute RouteRegister) {
prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), Wrap(SetHomeDashboard))
})
// Data sources
apiRoute.Group("/datasources", func(datasourceRoute RouteRegister) {
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))
apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
datasourceRoute.Get("/", Wrap(GetDataSources))
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
}, reqOrgAdmin)
apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn)
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
apiRoute.Get("/plugins", wrap(GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
apiRoute.Get("/plugins", Wrap(hs.GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
apiRoute.Get("/plugins/:pluginId/markdown/:name", Wrap(GetPluginMarkdown))
apiRoute.Group("/plugins", func(pluginRoute RouteRegister) {
pluginRoute.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
pluginRoute.Get("/:pluginId/dashboards/", Wrap(GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
}, reqOrgAdmin)
apiRoute.Get("/frontend/settings/", GetFrontendSettings)
apiRoute.Get("/frontend/settings/", hs.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))
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
folderRoute.Get("/", Wrap(GetFolders))
folderRoute.Get("/id/:id", Wrap(GetFolderByID))
folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(CreateFolder))
folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) {
folderUidRoute.Get("/", wrap(GetFolderByUid))
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
folderUidRoute.Delete("/", wrap(DeleteFolder))
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Get("/", Wrap(GetFolderByUID))
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), Wrap(UpdateFolder))
folderUidRoute.Delete("/", Wrap(DeleteFolder))
folderUidRoute.Group("/permissions", func(folderPermissionRoute RouteRegister) {
folderPermissionRoute.Get("/", wrap(GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", Wrap(GetFolderPermissionList))
folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateFolderPermissions))
})
})
})
// Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid))
apiRoute.Group("/dashboards", func(dashboardRoute routing.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.Get("/db/:slug", Wrap(GetDashboard))
dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboard))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
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.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))
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", Wrap(GetDashboardVersions))
dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion))
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute RouteRegister) {
dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateDashboardPermissions))
})
})
})
// Dashboard snapshots
apiRoute.Group("/dashboard/snapshots", func(dashboardRoute RouteRegister) {
dashboardRoute.Get("/", wrap(SearchDashboardSnapshots))
apiRoute.Group("/dashboard/snapshots", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/", Wrap(SearchDashboardSnapshots))
})
// Playlist
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))
apiRoute.Group("/playlists", func(playlistRoute routing.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
apiRoute.Get("/search/", Search)
// metrics
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))
apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(hs.QueryMetrics))
apiRoute.Get("/tsdb/testdata/scenarios", Wrap(GetTestDataScenarios))
apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, Wrap(GenerateSQLTestData))
apiRoute.Get("/tsdb/testdata/random-walk", Wrap(GetTestDataRandomWalk))
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))
apiRoute.Group("/alerts", func(alertsRoute routing.RouteRegister) {
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), Wrap(AlertTest))
alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), Wrap(PauseAlert))
alertsRoute.Get("/:alertId", ValidateOrgAlert, Wrap(GetAlert))
alertsRoute.Get("/", Wrap(GetAlerts))
alertsRoute.Get("/states-for-dashboard", Wrap(GetAlertStatesForDashboard))
})
apiRoute.Get("/alert-notifications", wrap(GetAlertNotifications))
apiRoute.Get("/alert-notifiers", wrap(GetAlertNotifiers))
apiRoute.Get("/alert-notifications", Wrap(GetAlertNotifications))
apiRoute.Get("/alert-notifiers", Wrap(GetAlertNotifiers))
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))
apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), Wrap(NotificationTest))
alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
alertNotifications.Get("/:notificationId", Wrap(GetAlertNotificationByID))
alertNotifications.Delete("/:notificationId", Wrap(DeleteAlertNotification))
}, reqEditorRole)
apiRoute.Get("/annotations", wrap(GetAnnotations))
apiRoute.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))
apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation))
annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation))
annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion))
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation))
})
// error test
r.Get("/metrics/error", wrap(GenerateError))
r.Get("/metrics/error", Wrap(GenerateError))
}, reqSignedIn)
// admin api
r.Group("/api/admin", func(adminRoute RouteRegister) {
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
adminRoute.Get("/settings", AdminGetSettings)
adminRoute.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
adminRoute.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
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("/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))
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
}, reqGrafanaAdmin)
// rendering
r.Get("/render/*", reqSignedIn, RenderToPng)
r.Get("/render/*", reqSignedIn, hs.RenderToPng)
// grafana.net proxy
r.Any("/api/gnet/*", reqSignedIn, ProxyGnetRequest)
@@ -380,10 +390,4 @@ func (hs *HttpServer) registerRoutes() {
// streams
//r.Post("/api/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream)
r.Register(macaronR)
InitAppPluginRoutes(macaronR)
macaronR.NotFound(NotFoundHandler)
}

View File

@@ -7,11 +7,11 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
func GetApiKeys(c *m.ReqContext) 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))
@@ -23,25 +23,25 @@ func GetApiKeys(c *m.ReqContext) Response {
}
}
return Json(200, result)
return JSON(200, result)
}
func DeleteApiKey(c *m.ReqContext) 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 *m.ReqContext, 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
@@ -50,12 +50,12 @@ func AddApiKey(c *m.ReqContext, 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)
}

View File

@@ -18,7 +18,7 @@ import (
var pluginProxyTransport *http.Transport
func InitAppPluginRoutes(r *macaron.Macaron) {
func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) {
pluginProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: setting.PluginAppsSkipVerifyTLS,
@@ -55,11 +55,11 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
}
}
func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler {
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)
}

View File

@@ -97,15 +97,6 @@ type CacheServer struct {
cache *gocache.Cache
}
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
}
}
return defaultValue
}
func (this *CacheServer) Handler(ctx *macaron.Context) {
urlPath := ctx.Req.URL.Path
hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
@@ -226,7 +217,7 @@ func (this *thunderTask) Fetch() {
this.Done()
}
var client *http.Client = &http.Client{
var client = &http.Client{
Timeout: time.Second * 2,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
@@ -258,9 +249,6 @@ func (this *thunderTask) fetch() error {
this.Avatar.data = &bytes.Buffer{}
writer := bufio.NewWriter(this.Avatar.data)
if _, err = io.Copy(writer, resp.Body); err != nil {
return err
}
return nil
_, err = io.Copy(writer, resp.Body)
return err
}

View File

@@ -11,10 +11,10 @@ import (
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)
}
)
@@ -30,7 +30,7 @@ type NormalResponse struct {
err error
}
func wrap(action interface{}) macaron.Handler {
func Wrap(action interface{}) macaron.Handler {
return func(c *m.ReqContext) {
var res Response
@@ -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{

View File

@@ -23,7 +23,7 @@ func loggedInUserScenarioWithRole(desc string, method string, url string, routeP
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
@@ -46,6 +46,31 @@ func loggedInUserScenarioWithRole(desc string, method string, url string, routeP
})
}
func anonymousUserScenario(desc string, method string, url string, routePattern string, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
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)
@@ -99,7 +124,7 @@ func setupScenarioContext(url string) *scenarioContext {
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
return sc
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"path"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -22,12 +23,16 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func isDashboardStarredByUser(c *m.ReqContext, dashId int64) (bool, error) {
const (
anonString = "Anonymous"
)
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
}
@@ -37,10 +42,10 @@ func isDashboardStarredByUser(c *m.ReqContext, dashId int64) (bool, error) {
func dashboardGuardianResponse(err error) Response {
if err != nil {
return ApiError(500, "Error while checking dashboard permissions", err)
return Error(500, "Error while checking dashboard permissions", err)
}
return ApiError(403, "Access denied to this dashboard", nil)
return Error(403, "Access denied to this dashboard", nil)
}
func GetDashboard(c *m.ReqContext) Response {
@@ -60,11 +65,11 @@ func GetDashboard(c *m.ReqContext) Response {
isStarred, err := isDashboardStarredByUser(c, dash.Id)
if err != nil {
return ApiError(500, "Error while checking if dashboard was starred by user", err)
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"
updater, creator := anonString, anonString
if dash.UpdatedBy > 0 {
updater = getUserLogin(dash.UpdatedBy)
}
@@ -96,12 +101,22 @@ func GetDashboard(c *m.ReqContext) Response {
if dash.FolderId > 0 {
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Dashboard folder could not be read", err)
return Error(500, "Dashboard folder could not be read", err)
}
meta.FolderTitle = query.Result.Title
meta.FolderUrl = query.Result.GetUrl()
}
isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id}
err = bus.Dispatch(isDashboardProvisioned)
if err != nil {
return Error(500, "Error while checking if dashboard is provisioned", err)
}
if isDashboardProvisioned.Result {
meta.Provisioned = true
}
// make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version)
@@ -111,31 +126,29 @@ func GetDashboard(c *m.ReqContext) Response {
}
c.TimeRequest(metrics.M_Api_Dashboard_Get)
return 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 anonString
}
return query.Result.Login
}
func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) {
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}
query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID}
} else {
query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgID}
}
if err := bus.Dispatch(&query); err != nil {
return nil, ApiError(404, "Dashboard not found", err)
return nil, Error(404, "Dashboard not found", err)
}
return query.Result, nil
@@ -145,11 +158,11 @@ func DeleteDashboard(c *m.ReqContext) Response {
query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to retrieve dashboards by slug", err)
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()})
return JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
}
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
@@ -164,16 +177,16 @@ func DeleteDashboard(c *m.ReqContext) Response {
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete dashboard", err)
return Error(500, "Failed to delete dashboard", err)
}
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
})
}
func DeleteDashboardByUid(c *m.ReqContext) Response {
func DeleteDashboardByUID(c *m.ReqContext) Response {
dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid"))
if rsp != nil {
return rsp
@@ -186,10 +199,10 @@ func DeleteDashboardByUid(c *m.ReqContext) Response {
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete dashboard", err)
return Error(500, "Failed to delete dashboard", err)
}
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
})
@@ -204,10 +217,10 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
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)
}
}
@@ -230,24 +243,25 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
err == m.ErrDashboardWithSameUIDExists ||
err == m.ErrFolderNotFound ||
err == m.ErrDashboardFolderCannotHaveParent ||
err == m.ErrDashboardFolderNameExists {
return ApiError(400, err.Error(), nil)
err == m.ErrDashboardFolderNameExists ||
err == m.ErrDashboardCannotSaveProvisionedDashboard {
return Error(400, err.Error(), nil)
}
if err == m.ErrDashboardUpdateAccessDenied {
return ApiError(403, err.Error(), err)
return Error(403, err.Error(), err)
}
if err == m.ErrDashboardContainsInvalidAlertData {
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
if validationErr, ok := err.(alerting.ValidationError); ok {
return Error(422, validationErr.Error(), nil)
}
if err != nil {
if err == m.ErrDashboardWithSameNameInFolderExists {
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
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 + "."
@@ -255,20 +269,20 @@ func PostDashboard(c *m.ReqContext, 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)
}
if err == m.ErrDashboardFailedToUpdateAlertData {
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
return Error(500, "Invalid alert data. Cannot save dashboard", err)
}
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"status": "success",
"slug": dashboard.Slug,
"version": dashboard.Version,
@@ -279,9 +293,9 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
}
func GetHomeDashboard(c *m.ReqContext) Response {
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
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 {
@@ -290,16 +304,15 @@ func GetHomeDashboard(c *m.ReqContext) Response {
if err == nil {
url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
return Json(200, &dashRedirect)
} else {
log.Warn("Failed to get slug from database, %s", err.Error())
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{}
@@ -309,14 +322,14 @@ func GetHomeDashboard(c *m.ReqContext) Response {
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) {
@@ -339,22 +352,22 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
// GetDashboardVersions returns all dashboard versions as JSON
func GetDashboardVersions(c *m.ReqContext) Response {
dashId := c.ParamsInt64(":dashboardId")
dashID := c.ParamsInt64(":dashboardId")
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
query := m.GetDashboardVersionsQuery{
OrgId: c.OrgId,
DashboardId: dashId,
DashboardId: dashID,
Limit: c.QueryInt("limit"),
Start: c.QueryInt("start"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err)
return Error(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err)
}
for _, version := range query.Result {
@@ -373,29 +386,29 @@ func GetDashboardVersions(c *m.ReqContext) Response {
}
}
return Json(200, query.Result)
return JSON(200, query.Result)
}
// GetDashboardVersion returns the dashboard version with the given ID.
func GetDashboardVersion(c *m.ReqContext) Response {
dashId := c.ParamsInt64(":dashboardId")
dashID := c.ParamsInt64(":dashboardId")
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
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,
DashboardId: dashID,
Version: c.ParamsInt(":id"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err)
return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
}
creator := "Anonymous"
creator := anonString
if query.Result.CreatedBy > 0 {
creator = getUserLogin(query.Result.CreatedBy)
}
@@ -405,7 +418,7 @@ func GetDashboardVersion(c *m.ReqContext) Response {
CreatedBy: creator,
}
return Json(200, dashVersionMeta)
return JSON(200, dashVersionMeta)
}
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
@@ -441,9 +454,9 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption
result, err := dashdiffs.CalculateDiff(&options)
if err != nil {
if err == m.ErrDashboardVersionNotFound {
return ApiError(404, "Dashboard version not found", err)
return Error(404, "Dashboard version not found", err)
}
return ApiError(500, "Unable to compute diff", err)
return Error(500, "Unable to compute diff", err)
}
if options.DiffType == dashdiffs.DiffDelta {
@@ -467,7 +480,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
if err := bus.Dispatch(&versionQuery); err != nil {
return ApiError(404, "Dashboard version not found", nil)
return Error(404, "Dashboard version not found", nil)
}
version := versionQuery.Result

View File

@@ -10,14 +10,14 @@ import (
)
func GetDashboardPermissionList(c *m.ReqContext) Response {
dashId := c.ParamsInt64(":dashboardId")
dashID := c.ParamsInt64(":dashboardId")
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
_, rsp := getDashboardHelper(c.OrgId, "", dashID, "")
if rsp != nil {
return rsp
}
g := guardian.New(dashId, c.OrgId, c.SignedInUser)
g := guardian.New(dashID, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
@@ -25,38 +25,43 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
acl, err := g.GetAcl()
if err != nil {
return ApiError(500, "Failed to get dashboard permissions", err)
return Error(500, "Failed to get dashboard permissions", err)
}
for _, perm := range acl {
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
if perm.TeamId > 0 {
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
}
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
}
return Json(200, acl)
return JSON(200, acl)
}
func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
dashId := c.ParamsInt64(":dashboardId")
dashID := c.ParamsInt64(":dashboardId")
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
_, rsp := getDashboardHelper(c.OrgId, "", dashID, "")
if rsp != nil {
return rsp
}
g := guardian.New(dashId, c.OrgId, c.SignedInUser)
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
cmd.DashboardId = dashID
for _, item := range apiCmd.Items {
cmd.Items = append(cmd.Items, &m.DashboardAcl{
OrgId: c.OrgId,
DashboardId: dashId,
DashboardId: dashID,
UserId: item.UserId,
TeamId: item.TeamId,
Role: item.Role,
@@ -70,21 +75,21 @@ func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclC
if err != nil {
if err == guardian.ErrGuardianPermissionExists ||
err == guardian.ErrGuardianOverride {
return ApiError(400, err.Error(), err)
return Error(400, err.Error(), err)
}
return ApiError(500, "Error while checking dashboard permissions", err)
return Error(500, "Error while checking dashboard permissions", err)
}
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
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 ApiError(409, err.Error(), err)
return Error(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
return Error(500, "Failed to create permission", err)
}
return ApiSuccess("Dashboard permissions updated")
return Success("Dashboard permissions updated")
}

View File

@@ -143,7 +143,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) {
})
})
Convey("When trying to override inherited permissions with lower presedence", func() {
Convey("When trying to override inherited permissions with lower precedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
@@ -194,7 +194,7 @@ func updateDashboardPermissionScenario(desc string, url string, routePattern str
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.OrgId = TestOrgID
sc.context.UserId = TestUserID

View File

@@ -91,40 +91,60 @@ func GetDashboardSnapshot(c *m.ReqContext) {
c.JSON(200, dto)
}
// GET /api/snapshots-delete/:key
func DeleteDashboardSnapshot(c *m.ReqContext) Response {
key := c.Params(":key")
// GET /api/snapshots-delete/:deleteKey
func DeleteDashboardSnapshotByDeleteKey(c *m.ReqContext) Response {
key := c.Params(":deleteKey")
query := &m.GetDashboardSnapshotQuery{DeleteKey: key}
err := bus.Dispatch(query)
if err != nil {
return ApiError(500, "Failed to get dashboard snapshot", err)
return Error(500, "Failed to get dashboard snapshot", err)
}
cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
if err := bus.Dispatch(cmd); err != nil {
return Error(500, "Failed to delete dashboard snapshot", err)
}
return JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches."})
}
// DELETE /api/snapshots/:key
func DeleteDashboardSnapshot(c *m.ReqContext) Response {
key := c.Params(":key")
query := &m.GetDashboardSnapshotQuery{Key: key}
err := bus.Dispatch(query)
if err != nil {
return Error(500, "Failed to get dashboard snapshot", err)
}
if query.Result == nil {
return ApiError(404, "Failed to get dashboard snapshot", nil)
return Error(404, "Failed to get dashboard snapshot", nil)
}
dashboard := query.Result.Dashboard
dashboardId := dashboard.Get("id").MustInt64()
dashboardID := dashboard.Get("id").MustInt64()
guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
canEdit, err := guardian.CanEdit()
if err != nil {
return ApiError(500, "Error while checking permissions for snapshot", err)
return Error(500, "Error while checking permissions for snapshot", err)
}
if !canEdit && query.Result.UserId != c.SignedInUser.UserId {
return ApiError(403, "Access denied to this snapshot", nil)
return Error(403, "Access denied to this snapshot", nil)
}
cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key}
cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
if err := bus.Dispatch(cmd); err != nil {
return ApiError(500, "Failed to delete dashboard snapshot", err)
return Error(500, "Failed to delete dashboard snapshot", err)
}
return 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 any CDN caches."})
}
// GET /api/dashboard/snapshots
@@ -145,7 +165,7 @@ func SearchDashboardSnapshots(c *m.ReqContext) Response {
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))
@@ -154,7 +174,6 @@ func SearchDashboardSnapshots(c *m.ReqContext) Response {
Id: snapshot.Id,
Name: snapshot.Name,
Key: snapshot.Key,
DeleteKey: snapshot.DeleteKey,
OrgId: snapshot.OrgId,
UserId: snapshot.UserId,
External: snapshot.External,
@@ -165,5 +184,5 @@ func SearchDashboardSnapshots(c *m.ReqContext) Response {
}
}
return Json(200, dtos)
return JSON(200, dtos)
}

View File

@@ -39,7 +39,7 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
return nil
})
teamResp := []*m.Team{}
teamResp := []*m.TeamDTO{}
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = teamResp
return nil
@@ -47,15 +47,30 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
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) {
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
Convey("When user is anonymous", func() {
Convey("Should be able to delete snapshot by deleteKey", func() {
anonymousUserScenario("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "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 dashboard has default ACL", func() {
aclMockResp = []*m.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
@@ -63,9 +78,9 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
}
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) {
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
@@ -81,9 +96,9 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
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) {
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
sc.handlerFunc = DeleteDashboardSnapshot
sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec()
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())

View File

@@ -9,6 +9,7 @@ import (
"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/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
@@ -42,6 +43,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
@@ -56,7 +62,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
query.Result = []*m.TeamDTO{}
return nil
})
@@ -105,7 +111,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
@@ -165,7 +171,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by uid", func() {
@@ -192,6 +198,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
fakeDash.HasAcl = true
setting.ViewersCanEdit = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{fakeDash}
query.Result = dashboards
@@ -220,7 +231,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = []*m.Team{}
query.Result = []*m.TeamDTO{}
return nil
})
@@ -271,7 +282,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
@@ -329,7 +340,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
@@ -398,7 +409,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by uid", func() {
@@ -468,7 +479,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
@@ -527,7 +538,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 200)
Convey("Should lookup dashboard by uid", func() {
@@ -594,7 +605,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
CallDeleteDashboardByUid(sc)
CallDeleteDashboardByUID(sc)
So(sc.resp.Code, ShouldEqual, 403)
Convey("Should lookup dashboard by uid", func() {
@@ -625,6 +636,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
dashTwo.FolderId = 3
dashTwo.HasAcl = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{dashOne, dashTwo}
query.Result = dashboards
@@ -638,7 +654,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
Convey("Should result in 412 Precondition failed", func() {
So(sc.resp.Code, ShouldEqual, 412)
result := sc.ToJson()
result := sc.ToJSON()
So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists")
So(result.Get("message").MustString(), ShouldEqual, m.ErrDashboardsWithSameSlugExists.Error())
})
@@ -686,7 +702,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
Convey("It should return correct response data", func() {
result := sc.ToJson()
result := sc.ToJSON()
So(result.Get("status").MustString(), ShouldEqual, "success")
So(result.Get("id").MustInt64(), ShouldEqual, 2)
So(result.Get("uid").MustString(), ShouldEqual, "uid")
@@ -710,7 +726,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412},
{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500},
{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
@@ -720,6 +736,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardCannotSaveProvisionedDashboard, ExpectedStatusCode: 400},
{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
}
@@ -750,6 +767,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{
Data: simplejson.NewFromAny(map[string]interface{}{
@@ -837,12 +859,12 @@ func CallDeleteDashboard(sc *scenarioContext) {
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
func CallDeleteDashboardByUid(sc *scenarioContext) {
func CallDeleteDashboardByUID(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
return nil
})
sc.handlerFunc = DeleteDashboardByUid
sc.handlerFunc = DeleteDashboardByUID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
}
@@ -861,7 +883,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
@@ -886,7 +908,7 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{
OrgId: TestOrgID,
@@ -903,7 +925,7 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
})
}
func (sc *scenarioContext) ToJson() *simplejson.Json {
func (sc *scenarioContext) ToJSON() *simplejson.Json {
var result *simplejson.Json
err := json.NewDecoder(sc.resp.Body).Decode(&result)
So(err, ShouldBeNil)

View File

@@ -1,47 +1,22 @@
package api
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m.DataSource, error) {
cacheKey := fmt.Sprintf("ds-%d", id)
if !nocache {
if cached, found := hs.cache.Get(cacheKey); found {
ds := cached.(*m.DataSource)
if ds.OrgId == orgId {
return ds, nil
}
}
}
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 (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) {
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
ds, err := hs.getDatasourceById(c.ParamsInt64(":id"), c.OrgId, nocache)
dsId := c.ParamsInt64(":id")
ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache)
if err != nil {
if err == m.ErrDataSourceAccessDenied {
c.JsonApiErr(403, "Access denied to datasource", err)
return
}
c.JsonApiErr(500, "Unable to load datasource meta data", err)
return
}
@@ -53,7 +28,21 @@ func (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) {
return
}
proxyPath := c.Params("*")
// macaron does not include trailing slashes when resolving a wildcard path
proxyPath := ensureProxyPathTrailingSlash(c.Req.URL.Path, c.Params("*"))
proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
proxy.HandleRequest()
}
// ensureProxyPathTrailingSlash Check for a trailing slash in original path and makes
// sure that a trailing slash is added to proxy path, if not already exists.
func ensureProxyPathTrailingSlash(originalPath, proxyPath string) string {
if len(proxyPath) > 1 {
if originalPath[len(originalPath)-1] == '/' && proxyPath[len(proxyPath)-1] != '/' {
return proxyPath + "/"
}
}
return proxyPath
}

19
pkg/api/dataproxy_test.go Normal file
View File

@@ -0,0 +1,19 @@
package api
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestDataProxy(t *testing.T) {
Convey("Data proxy test", t, func() {
Convey("Should append trailing slash to proxy path if original path has a trailing slash", func() {
So(ensureProxyPathTrailingSlash("/api/datasources/proxy/6/api/v1/query_range/", "api/v1/query_range/"), ShouldEqual, "api/v1/query_range/")
})
Convey("Should not append trailing slash to proxy path if original path doesn't have a trailing slash", func() {
So(ensureProxyPathTrailingSlash("/api/datasources/proxy/6/api/v1/query_range", "api/v1/query_range"), ShouldEqual, "api/v1/query_range")
})
})
}

View File

@@ -14,14 +14,14 @@ 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.DataSourceListItemDTO{
Id: ds.Id,
OrgId: ds.OrgId,
Id: ds.Id,
Name: ds.Name,
Url: ds.Url,
Type: ds.Type,
@@ -46,7 +46,7 @@ func GetDataSources(c *m.ReqContext) Response {
sort.Sort(result)
return Json(200, &result)
return JSON(200, &result)
}
func GetDataSourceById(c *m.ReqContext) Response {
@@ -57,66 +57,69 @@ func GetDataSourceById(c *m.ReqContext) 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 *m.ReqContext) Response {
id := c.ParamsInt64(":id")
if id <= 0 {
return ApiError(400, "Missing valid datasource id", nil)
return Error(400, "Missing valid datasource id", nil)
}
ds, err := getRawDataSourceById(id, c.OrgId)
if err != nil {
return ApiError(400, "Failed to delete datasource", nil)
return Error(400, "Failed to delete datasource", nil)
}
if ds.ReadOnly {
return ApiError(403, "Cannot delete read-only data source", nil)
return Error(403, "Cannot delete read-only data source", nil)
}
cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
err = bus.Dispatch(cmd)
if err != nil {
return ApiError(500, "Failed to delete datasource", err)
return Error(500, "Failed to delete datasource", err)
}
return ApiSuccess("Data source deleted")
return Success("Data source deleted")
}
func DeleteDataSourceByName(c *m.ReqContext) Response {
name := c.Params(":name")
if name == "" {
return ApiError(400, "Missing valid datasource name", nil)
return Error(400, "Missing valid datasource name", nil)
}
getCmd := &m.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
if err := bus.Dispatch(getCmd); err != nil {
return ApiError(500, "Failed to delete datasource", err)
if err == m.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to delete datasource", err)
}
if getCmd.Result.ReadOnly {
return ApiError(403, "Cannot delete read-only data source", nil)
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 {
return ApiError(500, "Failed to delete datasource", err)
return Error(500, "Failed to delete datasource", err)
}
return ApiSuccess("Data source deleted")
return Success("Data source deleted")
}
func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
@@ -124,14 +127,14 @@ func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDataSourceNameExists {
return ApiError(409, err.Error(), err)
return Error(409, err.Error(), err)
}
return ApiError(500, "Failed to add datasource", err)
return Error(500, "Failed to add datasource", err)
}
ds := convertModelToDtos(cmd.Result)
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"message": "Datasource added",
"id": cmd.Result.Id,
"name": cmd.Result.Name,
@@ -143,29 +146,42 @@ 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 {
if err == m.ErrDataSourceUpdatingOldVersion {
return ApiError(500, "Failed to update datasource. Reload new version and try again", err)
} else {
return ApiError(500, "Failed to update datasource", err)
return Error(500, "Failed to update datasource. Reload new version and try again", err)
}
return Error(500, "Failed to update datasource", err)
}
ds := convertModelToDtos(cmd.Result)
return Json(200, util.DynMap{
query := m.GetDataSourceByIdQuery{
Id: cmd.Id,
OrgId: c.OrgId,
}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrDataSourceNotFound {
return Error(404, "Data source not found", nil)
}
return Error(500, "Failed to query datasources", err)
}
dtos := convertModelToDtos(query.Result)
return JSON(200, util.DynMap{
"message": "Datasource updated",
"id": cmd.Id,
"name": cmd.Name,
"datasource": ds,
"datasource": dtos,
})
}
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
if len(cmd.SecureJsonData) == 0 {
return nil
}
@@ -179,8 +195,8 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
return m.ErrDatasourceIsReadOnly
}
secureJsonData := ds.SecureJsonData.Decrypt()
for k, v := range secureJsonData {
secureJSONData := ds.SecureJsonData.Decrypt()
for k, v := range secureJSONData {
if _, ok := cmd.SecureJsonData[k]; !ok {
cmd.SecureJsonData[k] = v
@@ -190,10 +206,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 {
@@ -209,14 +225,14 @@ func GetDataSourceByName(c *m.ReqContext) 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)
}
dtos := convertModelToDtos(query.Result)
dtos.ReadOnly = true
return Json(200, &dtos)
return JSON(200, &dtos)
}
// Get /api/datasources/id/:name
@@ -225,9 +241,9 @@ func GetDataSourceIdByName(c *m.ReqContext) 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
@@ -235,7 +251,7 @@ func GetDataSourceIdByName(c *m.ReqContext) Response {
Id: ds.Id,
}
return Json(200, &dtos)
return JSON(200, &dtos)
}
func convertModelToDtos(ds *m.DataSource) dtos.DataSource {

View File

@@ -46,5 +46,13 @@ func TestDataSourcesProxy(t *testing.T) {
So(respJSON[3]["name"], ShouldEqual, "ZZZ")
})
})
Convey("Should be able to save a data source", func() {
loggedInUserScenario("When calling DELETE on non-existing", "/api/datasources/name/12345", func(sc *scenarioContext) {
sc.handlerFunc = DeleteDataSourceByName
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 404)
})
})
})
}

View File

@@ -1,35 +1,78 @@
package dtos
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models"
)
type AlertRule struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
Name string `json:"name"`
Message string `json:"message"`
State m.AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
EvalDate time.Time `json:"evalDate"`
EvalData *simplejson.Json `json:"evalData"`
ExecutionError string `json:"executionError"`
Url string `json:"url"`
CanEdit bool `json:"canEdit"`
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
Name string `json:"name"`
Message string `json:"message"`
State models.AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
EvalDate time.Time `json:"evalDate"`
EvalData *simplejson.Json `json:"evalData"`
ExecutionError string `json:"executionError"`
Url string `json:"url"`
CanEdit bool `json:"canEdit"`
}
func formatShort(interval time.Duration) string {
var result string
hours := interval / time.Hour
if hours > 0 {
result += fmt.Sprintf("%dh", hours)
}
remaining := interval - (hours * time.Hour)
mins := remaining / time.Minute
if mins > 0 {
result += fmt.Sprintf("%dm", mins)
}
remaining = remaining - (mins * time.Minute)
seconds := remaining / time.Second
if seconds > 0 {
result += fmt.Sprintf("%ds", seconds)
}
return result
}
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
return &AlertNotification{
Id: notification.Id,
Name: notification.Name,
Type: notification.Type,
IsDefault: notification.IsDefault,
Created: notification.Created,
Updated: notification.Updated,
Frequency: formatShort(notification.Frequency),
SendReminder: notification.SendReminder,
DisableResolveMessage: notification.DisableResolveMessage,
Settings: notification.Settings,
}
}
type AlertNotification struct {
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
IsDefault bool `json:"isDefault"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
IsDefault bool `json:"isDefault"`
SendReminder bool `json:"sendReminder"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Frequency string `json:"frequency"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Settings *simplejson.Json `json:"settings"`
}
type AlertTestCommand struct {
@@ -39,7 +82,7 @@ type AlertTestCommand struct {
type AlertTestResult struct {
Firing bool `json:"firing"`
State m.AlertStateType `json:"state"`
State models.AlertStateType `json:"state"`
ConditionEvals string `json:"conditionEvals"`
TimeMs string `json:"timeMs"`
Error string `json:"error,omitempty"`
@@ -59,9 +102,12 @@ type EvalMatch struct {
}
type NotificationTestCommand struct {
Name string `json:"name"`
Type string `json:"type"`
Settings *simplejson.Json `json:"settings"`
Name string `json:"name"`
Type string `json:"type"`
SendReminder bool `json:"sendReminder"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Frequency string `json:"frequency"`
Settings *simplejson.Json `json:"settings"`
}
type PauseAlertCommand struct {

View File

@@ -0,0 +1,35 @@
package dtos
import (
"testing"
"time"
)
func TestFormatShort(t *testing.T) {
tcs := []struct {
interval time.Duration
expected string
}{
{interval: time.Hour, expected: "1h"},
{interval: time.Hour + time.Minute, expected: "1h1m"},
{interval: (time.Hour * 10) + time.Minute, expected: "10h1m"},
{interval: (time.Hour * 10) + (time.Minute * 10) + time.Second, expected: "10h10m1s"},
{interval: time.Minute * 10, expected: "10m"},
}
for _, tc := range tcs {
got := formatShort(tc.interval)
if got != tc.expected {
t.Errorf("expected %s got %s interval: %v", tc.expected, got, tc.interval)
}
parsed, err := time.ParseDuration(tc.expected)
if err != nil {
t.Fatalf("could not parse expected duration")
}
if parsed != tc.interval {
t.Errorf("expects the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval)
}
}
}

View File

@@ -28,6 +28,7 @@ type DashboardMeta struct {
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
}
type DashboardFullWithMeta struct {

View File

@@ -13,6 +13,8 @@ type IndexViewData struct {
Theme string
NewGrafanaVersionExists bool
NewGrafanaVersion string
AppName string
AppNameBodyClass string
}
type PluginCss struct {

View File

@@ -22,21 +22,22 @@ type LoginCommand struct {
}
type CurrentUser struct {
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
Login string `json:"login"`
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"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"`
Timezone string `json:"timezone"`
Locale string `json:"locale"`
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
Login string `json:"login"`
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"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"`
Timezone string `json:"timezone"`
Locale string `json:"locale"`
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"`
}
type MetricRequest struct {
@@ -50,6 +51,10 @@ type UserStars struct {
}
func GetGravatarUrl(text string) string {
if setting.DisableGravatar {
return setting.AppSubUrl + "/public/img/user_profile.png"
}
if text == "" {
return ""
}

View File

@@ -19,9 +19,9 @@ type PluginSetting struct {
JsonData map[string]interface{} `json:"jsonData"`
DefaultNavUrl string `json:"defaultNavUrl"`
LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"`
State string `json:"state"`
LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"`
State plugins.PluginState `json:"state"`
}
type PluginListItem struct {
@@ -34,7 +34,7 @@ type PluginListItem struct {
LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"`
DefaultNavUrl string `json:"defaultNavUrl"`
State string `json:"state"`
State plugins.PluginState `json:"state"`
}
type PluginList []PluginListItem
@@ -57,4 +57,5 @@ type ImportDashboardCommand struct {
Overwrite bool `json:"overwrite"`
Dashboard *simplejson.Json `json:"dashboard"`
Inputs []plugins.ImportDashboardInput `json:"inputs"`
FolderId int64 `json:"folderId"`
}

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"`
}

View File

@@ -28,30 +28,30 @@ func GetFolders(c *m.ReqContext) Response {
})
}
return Json(200, result)
return JSON(200, result)
}
func GetFolderByUid(c *m.ReqContext) Response {
func GetFolderByUID(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUid(c.Params(":uid"))
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))
return JSON(200, toFolderDto(g, folder))
}
func GetFolderById(c *m.ReqContext) Response {
func GetFolderByID(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderById(c.ParamsInt64(":id"))
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))
return JSON(200, toFolderDto(g, folder))
}
func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
@@ -62,7 +62,7 @@ func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
}
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
return Json(200, toFolderDto(g, cmd.Result))
return JSON(200, toFolderDto(g, cmd.Result))
}
func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
@@ -73,7 +73,7 @@ func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
}
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
return Json(200, toFolderDto(g, cmd.Result))
return JSON(200, toFolderDto(g, cmd.Result))
}
func DeleteFolder(c *m.ReqContext) Response {
@@ -83,7 +83,7 @@ func DeleteFolder(c *m.ReqContext) Response {
return toFolderError(err)
}
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"title": f.Title,
"message": fmt.Sprintf("Folder %s deleted", f.Title),
})
@@ -95,7 +95,7 @@ func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder {
canAdmin, _ := g.CanAdmin()
// Finding creator and last updater of the folder
updater, creator := "Anonymous", "Anonymous"
updater, creator := anonString, anonString
if folder.CreatedBy > 0 {
creator = getUserLogin(folder.CreatedBy)
}
@@ -127,20 +127,20 @@ func toFolderError(err error) Response {
err == m.ErrDashboardTypeMismatch ||
err == m.ErrDashboardInvalidUid ||
err == m.ErrDashboardUidToLong {
return ApiError(400, err.Error(), nil)
return Error(400, err.Error(), nil)
}
if err == m.ErrFolderAccessDenied {
return ApiError(403, "Access denied", err)
return Error(403, "Access denied", err)
}
if err == m.ErrFolderNotFound {
return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
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 JSON(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
}
return ApiError(500, "Folder API error", err)
return Error(500, "Folder API error", err)
}

View File

@@ -12,7 +12,7 @@ import (
func GetFolderPermissionList(c *m.ReqContext) Response {
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
folder, err := s.GetFolderByUid(c.Params(":uid"))
folder, err := s.GetFolderByUID(c.Params(":uid"))
if err != nil {
return toFolderError(err)
@@ -26,24 +26,30 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
acl, err := g.GetAcl()
if err != nil {
return ApiError(500, "Failed to get folder permissions", err)
return Error(500, "Failed to get folder permissions", err)
}
for _, perm := range acl {
perm.FolderId = folder.Id
perm.DashboardId = 0
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
if perm.TeamId > 0 {
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
}
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
}
return Json(200, acl)
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"))
folder, err := s.GetFolderByUID(c.Params(":uid"))
if err != nil {
return toFolderError(err)
@@ -79,13 +85,13 @@ func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclComm
if err != nil {
if err == guardian.ErrGuardianPermissionExists ||
err == guardian.ErrGuardianOverride {
return ApiError(400, err.Error(), err)
return Error(400, err.Error(), err)
}
return ApiError(500, "Error while checking folder permissions", err)
return Error(500, "Error while checking folder permissions", err)
}
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
return Error(403, "Cannot remove own admin permission for a folder", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
@@ -97,11 +103,11 @@ func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclComm
}
if err == m.ErrFolderAclInfoMissing || err == m.ErrFolderPermissionFolderEmpty {
return ApiError(409, err.Error(), err)
return Error(409, err.Error(), err)
}
return ApiError(500, "Failed to create permission", err)
return Error(500, "Failed to create permission", err)
}
return ApiSuccess("Folder permissions updated")
return Success("Folder permissions updated")
}

View File

@@ -17,7 +17,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
Convey("Folder permissions test", t, func() {
Convey("Given folder not exists", func() {
mock := &fakeFolderService{
GetFolderByUidError: m.ErrFolderNotFound,
GetFolderByUIDError: m.ErrFolderNotFound,
}
origNewFolderService := dashboards.NewFolderService
@@ -49,7 +49,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
@@ -96,7 +96,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
})
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
@@ -142,7 +142,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
})
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
@@ -178,7 +178,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
)
mock := &fakeFolderService{
GetFolderByUidResult: &m.Folder{
GetFolderByUIDResult: &m.Folder{
Id: 1,
Uid: "uid",
Title: "Folder",
@@ -226,7 +226,7 @@ func updateFolderPermissionScenario(desc string, url string, routePattern string
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.OrgId = TestOrgID
sc.context.UserId = TestUserID

View File

@@ -133,16 +133,6 @@ func TestFoldersApiEndpoint(t *testing.T) {
})
}
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()
}
@@ -152,7 +142,7 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
@@ -181,7 +171,7 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
@@ -204,10 +194,10 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa
type fakeFolderService struct {
GetFoldersResult []*m.Folder
GetFoldersError error
GetFolderByUidResult *m.Folder
GetFolderByUidError error
GetFolderByIdResult *m.Folder
GetFolderByIdError error
GetFolderByUIDResult *m.Folder
GetFolderByUIDError error
GetFolderByIDResult *m.Folder
GetFolderByIDError error
CreateFolderResult *m.Folder
CreateFolderError error
UpdateFolderResult *m.Folder
@@ -221,12 +211,12 @@ 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) 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) GetFolderByUID(uid string) (*m.Folder, error) {
return s.GetFolderByUIDResult, s.GetFolderByUIDError
}
func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
@@ -234,7 +224,7 @@ func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
return s.CreateFolderError
}
func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *m.UpdateFolderCommand) error {
func (s *fakeFolderService) UpdateFolder(existingUID string, cmd *m.UpdateFolderCommand) error {
cmd.Result = s.UpdateFolderResult
return s.UpdateFolderError
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
orgDataSources := make([]*m.DataSource, 0)
if c.OrgId != 0 {
@@ -22,7 +22,20 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
return nil, err
}
orgDataSources = query.Result
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
User: c.SignedInUser,
Datasources: query.Result,
}
if err := bus.Dispatch(&dsFilterQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return nil, err
}
orgDataSources = query.Result
} else {
orgDataSources = dsFilterQuery.Result
}
}
datasources := make(map[string]interface{})
@@ -120,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
panels := map[string]interface{}{}
for _, panel := range enabledPlugins.Panels {
if panel.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels {
continue
}
panels[panel.Id] = map[string]interface{}{
"module": panel.Module,
"baseUrl": panel.BaseUrl,
@@ -132,19 +149,22 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
}
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,
"externalUserMngInfo": setting.ExternalUserMngInfo,
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
"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,
"alertingErrorOrTimeout": setting.AlertingErrorOrTimeout,
"alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues,
"exploreEnabled": setting.ExploreEnabled,
"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,
@@ -152,6 +172,7 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
"latestVersion": plugins.GrafanaLatestVersion,
"hasUpdate": plugins.GrafanaHasUpdate,
"env": setting.Env,
"isEnterprise": setting.IsEnterprise,
},
}
@@ -179,8 +200,8 @@ func getPanelSort(id string) int {
return sort
}
func GetFrontendSettings(c *m.ReqContext) {
settings, err := getFrontendSettingsMap(c)
func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) {
settings, err := hs.getFrontendSettingsMap(c)
if err != nil {
c.JsonApiErr(400, "Failed to get frontend settings", err)
return

View File

@@ -11,11 +11,11 @@ import (
"path"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"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"
@@ -26,40 +26,71 @@ import (
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting"
)
type HttpServer struct {
func init() {
registry.Register(&registry.Descriptor{
Name: "HTTPServer",
Instance: &HTTPServer{},
InitPriority: registry.High,
})
}
type HTTPServer struct {
log log.Logger
macaron *macaron.Macaron
context context.Context
streamManager *live.StreamManager
cache *gocache.Cache
httpSrv *http.Server
httpSrv *http.Server
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""`
CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""`
}
func NewHttpServer() *HttpServer {
return &HttpServer{
log: log.New("http.server"),
cache: gocache.New(5*time.Minute, 10*time.Minute),
}
}
func (hs *HTTPServer) Init() error {
hs.log = log.New("http.server")
func (hs *HttpServer) Start(ctx context.Context) error {
var err error
hs.context = ctx
hs.streamManager = live.NewStreamManager()
hs.macaron = hs.newMacaron()
hs.registerRoutes()
return nil
}
func (hs *HTTPServer) Run(ctx context.Context) error {
var err error
hs.context = ctx
hs.applyRoutes()
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, "socket", setting.SocketPath)
hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
// handle http shutdown on server context done
go func() {
<-ctx.Done()
// Hacky fix for race condition between ListenAndServe and Shutdown
time.Sleep(time.Millisecond * 100)
if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
hs.log.Error("Failed to shutdown server", "error", err)
}
}()
switch setting.Protocol {
case setting.HTTP:
err = hs.httpSrv.ListenAndServe()
@@ -74,12 +105,15 @@ func (hs *HttpServer) Start(ctx context.Context) error {
return nil
}
case setting.SOCKET:
ln, err := net.Listen("unix", setting.SocketPath)
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")
@@ -93,13 +127,7 @@ func (hs *HttpServer) Start(ctx context.Context) error {
return err
}
func (hs *HttpServer) Shutdown(ctx context.Context) error {
err := hs.httpSrv.Shutdown(ctx)
hs.log.Info("Stopped HTTP server")
return err
}
func (hs *HttpServer) listenAndServeTLS(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")
}
@@ -136,15 +164,35 @@ func (hs *HttpServer) listenAndServeTLS(certfile, keyfile string) error {
}
hs.httpSrv.TLSConfig = tlsCfg
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
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()
// automatically set HEAD for every GET
m.SetAutoHead(true)
return m
}
func (hs *HTTPServer) applyRoutes() {
// start with middlewares & static routes
hs.addMiddlewaresAndStaticRoutes()
// then add view routes & api routes
hs.RouteRegister.Register(hs.macaron)
// then custom app proxy routes
hs.initAppPluginRoutes(hs.macaron)
// lastly not found route
hs.macaron.NotFound(hs.NotFoundHandler)
}
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.macaron
m.Use(middleware.Logger())
if setting.EnableGzip {
@@ -156,14 +204,15 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
for _, route := range plugins.StaticRoutes {
pluginRoute := path.Join("/public/plugins/", route.PluginId)
hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
hs.mapStatic(m, route.Directory, "", pluginRoute)
hs.mapStatic(hs.macaron, route.Directory, "", pluginRoute)
}
hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
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")
hs.mapStatic(m, hs.Cfg.ImagesDir, "", "/public/img/attachments")
}
m.Use(macaron.Renderer(macaron.RenderOptions{
@@ -175,7 +224,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
m.Use(hs.healthHandler)
m.Use(hs.metricsEndpoint)
m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(&setting.SessionOptions))
m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
m.Use(middleware.OrgRedirect())
// needs to be after context handler
@@ -183,12 +232,15 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
m.Use(middleware.ValidateHostHeader(setting.Domain))
}
m.Use(middleware.HandleNoCacheHeader())
m.Use(middleware.AddDefaultResponseHeaders())
return m
}
func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
if !hs.Cfg.MetricsEndpointEnabled {
return
}
if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" {
return
}
@@ -197,7 +249,7 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
ServeHTTP(ctx.Resp, ctx.Req.Request)
}
func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
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
@@ -221,11 +273,17 @@ func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
ctx.Resp.Write(dataBytes)
}
func (hs *HttpServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
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")
}
if prefix == "public/build" {
headers = func(c *macaron.Context) {
c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
}
}
if setting.Env == setting.DEV {
headers = func(c *macaron.Context) {
c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")

View File

@@ -11,13 +11,19 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
settings, err := getFrontendSettingsMap(c)
const (
// Themes
lightName = "light"
darkName = "dark"
)
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
settings, err := hs.getFrontendSettingsMap(c)
if err != nil {
return nil, err
}
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
if err := bus.Dispatch(&prefsQuery); err != nil {
return nil, err
}
@@ -32,44 +38,52 @@ func setIndexViewData(c *m.ReqContext) (*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"] = ""
}
hasEditPermissionInFoldersQuery := m.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := bus.Dispatch(&hasEditPermissionInFoldersQuery); err != nil {
return nil, err
}
var data = dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: c.UserId,
IsSignedIn: c.IsSignedIn,
Login: c.Login,
Email: c.Email,
Name: c.Name,
OrgCount: c.OrgCount,
OrgId: c.OrgId,
OrgName: c.OrgName,
OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email),
IsGrafanaAdmin: c.IsGrafanaAdmin,
LightTheme: prefs.Theme == "light",
Timezone: prefs.Timezone,
Locale: locale,
HelpFlags1: c.HelpFlags1,
Id: c.UserId,
IsSignedIn: c.IsSignedIn,
Login: c.Login,
Email: c.Email,
Name: c.Name,
OrgCount: c.OrgCount,
OrgId: c.OrgId,
OrgName: c.OrgName,
OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email),
IsGrafanaAdmin: c.IsGrafanaAdmin,
LightTheme: prefs.Theme == lightName,
Timezone: prefs.Timezone,
Locale: locale,
HelpFlags1: c.HelpFlags1,
HasEditPermissionInFolders: hasEditPermissionInFoldersQuery.Result,
},
Settings: settings,
Theme: prefs.Theme,
AppUrl: appUrl,
AppSubUrl: appSubUrl,
AppUrl: appURL,
AppSubUrl: appSubURL,
GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleTagManagerId: setting.GoogleTagManagerId,
BuildVersion: setting.BuildVersion,
BuildCommit: setting.BuildCommit,
NewGrafanaVersion: plugins.GrafanaLatestVersion,
NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
AppName: setting.ApplicationName,
AppNameBodyClass: getAppNameBodyClass(setting.ApplicationName),
}
if setting.DisableGravatar {
@@ -80,23 +94,32 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
data.User.Name = data.User.Login
}
themeUrlParam := c.Query("theme")
if themeUrlParam == "light" {
themeURLParam := c.Query("theme")
if themeURLParam == lightName {
data.User.LightTheme = true
data.Theme = "light"
data.Theme = lightName
} else if themeURLParam == darkName {
data.User.LightTheme = false
data.Theme = darkName
}
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
if hasEditPermissionInFoldersQuery.Result {
children := []*dtos.NavLink{
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
}
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
children = append(children, &dtos.NavLink{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"})
}
children = append(children, &dtos.NavLink{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/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"},
},
Text: "Create",
Id: "create",
Icon: "fa fa-fw fa-plus",
Url: setting.AppSubUrl + "/dashboard/new",
Children: children,
})
}
@@ -117,10 +140,28 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
Children: dashboardChildNavs,
})
if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Explore",
Id: "explore",
SubTitle: "Explore your data",
Icon: "fa fa-rocket",
Url: setting.AppSubUrl + "/explore",
Children: []*dtos.NavLink{
{Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore"},
},
})
}
if c.IsSignedIn {
// Only set login if it's different from the name
var login string
if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
login = c.SignedInUser.Login
}
profileNode := &dtos.NavLink{
Text: c.SignedInUser.NameOrFallback(),
SubTitle: c.SignedInUser.Login,
SubTitle: login,
Id: "profile",
Img: data.User.GravatarUrl,
Url: setting.AppSubUrl + "/profile",
@@ -204,7 +245,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
}
}
if c.OrgRole == m.ROLE_ADMIN {
if c.IsGrafanaAdmin || c.OrgRole == m.ROLE_ADMIN {
cfgNode := &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
@@ -258,10 +299,24 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
},
}
if c.IsGrafanaAdmin {
if c.OrgRole != m.ROLE_ADMIN {
cfgNode = &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "gicon gicon-cog",
Url: setting.AppSubUrl + "/admin/users",
Children: make([]*dtos.NavLink, 0),
}
}
if c.OrgRole == m.ROLE_ADMIN && c.IsGrafanaAdmin {
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
})
}
if c.IsGrafanaAdmin {
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Text: "Server Admin",
HideFromTabs: true,
@@ -284,6 +339,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
Id: "help",
Url: "#",
Icon: "gicon gicon-question",
@@ -295,28 +351,41 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
},
})
hs.HooksService.RunIndexDataHooks(&data)
return &data, nil
}
func Index(c *m.ReqContext) {
if data, err := setIndexViewData(c); err != nil {
func (hs *HTTPServer) Index(c *m.ReqContext) {
data, err := hs.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 *m.ReqContext) {
func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) {
if c.IsApiRequest() {
c.JsonApiErr(404, "Not found", nil)
return
}
if data, err := setIndexViewData(c); err != nil {
data, err := hs.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)
}
func getAppNameBodyClass(name string) string {
switch name {
case setting.APP_NAME:
return "app-grafana"
case setting.APP_NAME_ENTERPRISE:
return "app-enterprise"
default:
return ""
}
}

View File

@@ -70,7 +70,7 @@ func (c *connection) readPump() {
func (c *connection) handleMessage(message []byte) {
json, err := simplejson.NewJson(message)
if err != nil {
log.Error(3, "Unreadable message on websocket channel:", err)
log.Error(3, "Unreadable message on websocket channel. error: %v", err)
}
msgType := json.Get("action").MustString()

View File

@@ -37,9 +37,6 @@ func newHub() *hub {
}
}
func (h *hub) removeConnection() {
}
func (h *hub) run(ctx context.Context) {
for {
select {

View File

@@ -14,11 +14,11 @@ import (
)
const (
VIEW_INDEX = "index"
ViewIndex = "index"
)
func LoginView(c *m.ReqContext) {
viewData, err := setIndexViewData(c)
func (hs *HTTPServer) LoginView(c *m.ReqContext) {
viewData, err := hs.setIndexViewData(c)
if err != nil {
c.Handle(500, "Failed to get settings", err)
return
@@ -40,7 +40,7 @@ func LoginView(c *m.ReqContext) {
}
if !tryLoginUsingRememberCookie(c) {
c.HTML(200, VIEW_INDEX, viewData)
c.HTML(200, ViewIndex, viewData)
return
}
@@ -78,7 +78,13 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
user := userQuery.Result
// validate remember me cookie
if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login {
signingKey := user.Rands + user.Password
if len(signingKey) < 10 {
c.Logger.Error("Invalid user signingKey")
return false
}
if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
return false
}
@@ -87,7 +93,7 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
return true
}
func LoginApiPing(c *m.ReqContext) {
func LoginAPIPing(c *m.ReqContext) {
if !tryLoginUsingRememberCookie(c) {
c.JsonApiErr(401, "Unauthorized", nil)
return
@@ -98,21 +104,22 @@ func LoginApiPing(c *m.ReqContext) {
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,
IpAddress: c.Req.RemoteAddr,
authQuery := &m.LoginUserQuery{
ReqContext: c,
Username: cmd.User,
Password: cmd.Password,
IpAddress: c.Req.RemoteAddr,
}
if err := bus.Dispatch(&authQuery); err != nil {
if err := bus.Dispatch(authQuery); err != nil {
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
return ApiError(401, "Invalid username or password", err)
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
@@ -130,7 +137,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
metrics.M_Api_Login_Post.Inc()
return Json(200, result)
return JSON(200, result)
}
func loginUserWithUser(user *m.User, c *m.ReqContext) {
@@ -154,5 +161,9 @@ func Logout(c *m.ReqContext) {
c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
c.Session.Destory(c.Context)
c.Redirect(setting.AppSubUrl + "/login")
if setting.SignoutRedirectUrl != "" {
c.Redirect(setting.SignoutRedirectUrl)
} else {
c.Redirect(setting.AppSubUrl + "/login")
}
}

View File

@@ -6,7 +6,6 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net/http"
@@ -16,22 +15,15 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
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")
)
var oauthLogger = log.New("oauth")
func GenStateString() string {
rnd := make([]byte, 32)
@@ -56,7 +48,7 @@ func OAuthLogin(ctx *m.ReqContext) {
if errorParam != "" {
errorDesc := ctx.Query("error_description")
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
return
}
@@ -86,6 +78,7 @@ func OAuthLogin(ctx *m.ReqContext) {
// handle call back
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: setting.OAuthService.OAuthInfos[name].TlsSkipVerify,
},
@@ -149,54 +142,43 @@ func OAuthLogin(ctx *m.ReqContext) {
// validate that we got at least an email address
if userInfo.Email == "" {
redirectWithError(ctx, ErrNoEmail)
redirectWithError(ctx, login.ErrNoEmail)
return
}
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
redirectWithError(ctx, ErrEmailNotAllowed)
redirectWithError(ctx, login.ErrEmailNotAllowed)
return
}
userQuery := m.GetUserByEmailQuery{Email: userInfo.Email}
err = bus.Dispatch(&userQuery)
extUser := &m.ExternalUserInfo{
AuthModule: "oauth_" + name,
AuthId: userInfo.Id,
Name: userInfo.Name,
Login: userInfo.Login,
Email: userInfo.Email,
OrgRoles: map[int64]m.RoleType{},
}
// create account if missing
if err == m.ErrUserNotFound {
if !connect.IsSignupAllowed() {
redirectWithError(ctx, ErrSignUpNotAllowed)
return
}
limitReached, err := quota.QuotaReached(ctx, "user")
if err != nil {
ctx.Handle(500, "Failed to get user quota", err)
return
}
if limitReached {
redirectWithError(ctx, ErrUsersQuotaReached)
return
}
cmd := m.CreateUserCommand{
Login: userInfo.Login,
Email: userInfo.Email,
Name: userInfo.Name,
Company: userInfo.Company,
DefaultOrgRole: userInfo.Role,
}
if userInfo.Role != "" {
extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
}
if err = bus.Dispatch(&cmd); err != nil {
ctx.Handle(500, "Failed to create account", err)
return
}
userQuery.Result = &cmd.Result
} else if err != nil {
ctx.Handle(500, "Unexpected error", err)
// add/update user in grafana
cmd := &m.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: connect.IsSignupAllowed(),
}
err = bus.Dispatch(cmd)
if err != nil {
redirectWithError(ctx, err)
return
}
// login
loginUserWithUser(userQuery.Result, ctx)
loginUserWithUser(cmd.Result, ctx)
metrics.M_Api_Login_OAuth.Inc()

View File

@@ -13,21 +13,24 @@ import (
)
// POST /api/tsdb/query
func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
if len(reqDto.Queries) == 0 {
return ApiError(400, "No queries found in query", nil)
return Error(400, "No queries found in query", nil)
}
dsId, err := reqDto.Queries[0].Get("datasourceId").Int64()
datasourceId, err := reqDto.Queries[0].Get("datasourceId").Int64()
if err != nil {
return ApiError(400, "Query missing datasourceId", nil)
return Error(400, "Query missing datasourceId", nil)
}
dsQuery := m.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId}
if err := bus.Dispatch(&dsQuery); err != nil {
return ApiError(500, "failed to fetch data source", err)
ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache)
if err != nil {
if err == m.ErrDataSourceAccessDenied {
return Error(403, "Access denied to datasource", err)
}
return Error(500, "Unable to load datasource meta data", err)
}
request := &tsdb.TsdbQuery{TimeRange: timeRange}
@@ -38,13 +41,13 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
IntervalMs: query.Get("intervalMs").MustInt64(1000),
Model: query,
DataSource: dsQuery.Result,
DataSource: ds,
})
}
resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request)
resp, err := tsdb.HandleRequest(c.Req.Context(), ds, request)
if err != nil {
return ApiError(500, "Metric request error", err)
return Error(500, "Metric request error", err)
}
statusCode := 200
@@ -52,11 +55,11 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
if res.Error != nil {
res.ErrorString = res.Error.Error()
resp.Message = res.ErrorString
statusCode = 500
statusCode = 400
}
}
return Json(statusCode, &resp)
return JSON(statusCode, &resp)
}
// GET /api/tsdb/testdata/scenarios
@@ -72,22 +75,22 @@ func GetTestDataScenarios(c *m.ReqContext) Response {
})
}
return Json(200, &result)
return JSON(200, &result)
}
// Genereates a index out of range error
// Generates a index out of range error
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 {
func GenerateSQLTestData(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil {
return ApiError(500, "Failed to insert test data", err)
return Error(500, "Failed to insert test data", err)
}
return Json(200, &util.DynMap{"message": "OK"})
return JSON(200, &util.DynMap{"message": "OK"})
}
// GET /api/tsdb/testdata/random-walk
@@ -99,7 +102,7 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response {
timeRange := tsdb.NewTimeRange(from, to)
request := &tsdb.TsdbQuery{TimeRange: timeRange}
dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"}
dsInfo := &m.DataSource{Type: "testdata"}
request.Queries = append(request.Queries, &tsdb.Query{
RefId: "A",
IntervalMs: intervalMs,
@@ -111,8 +114,8 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response {
resp, err := tsdb.HandleRequest(context.Background(), dsInfo, request)
if err != nil {
return ApiError(500, "Metric request error", err)
return Error(500, "Metric request error", err)
}
return Json(200, &resp)
return JSON(200, &resp)
}

View File

@@ -15,7 +15,7 @@ func GetOrgCurrent(c *m.ReqContext) Response {
}
// GET /api/orgs/:orgId
func GetOrgById(c *m.ReqContext) Response {
func GetOrgByID(c *m.ReqContext) Response {
return getOrgHelper(c.ParamsInt64(":orgId"))
}
@@ -24,10 +24,10 @@ 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{
@@ -43,18 +43,18 @@ func GetOrgByName(c *m.ReqContext) 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
@@ -71,26 +71,26 @@ func getOrgHelper(orgId int64) Response {
},
}
return Json(200, &result)
return JSON(200, &result)
}
// POST /api/orgs
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()
return Json(200, &util.DynMap{
return JSON(200, &util.DynMap{
"orgId": cmd.Result.Id,
"message": "Organization created",
})
@@ -106,16 +106,16 @@ 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
@@ -128,9 +128,9 @@ 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,
@@ -142,21 +142,21 @@ 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 *m.ReqContext) 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 *m.ReqContext) Response {
@@ -168,8 +168,8 @@ func SearchOrgs(c *m.ReqContext) 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)
}

View File

@@ -16,30 +16,30 @@ 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 *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)
@@ -56,7 +56,7 @@ func AddOrgInvite(c *m.ReqContext, 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
@@ -74,18 +74,21 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
}
if err := bus.Dispatch(&emailCmd); err != nil {
return ApiError(500, "Failed to send email invite", err)
if err == m.ErrSmtpNotEnabled {
return Error(412, err.Error(), err)
}
return Error(500, "Failed to send email invite", err)
}
emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
if err := bus.Dispatch(&emailSentCmd); err != nil {
return ApiError(500, "Failed to update invite with email sent info", err)
return Error(500, "Failed to update invite with email sent info", err)
}
return ApiSuccess(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
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 *m.ReqContext, user *m.User, inviteDto *dtos.AddInviteForm) Response {
@@ -93,29 +96,28 @@ func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddI
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.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 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 *m.ReqContext) Response {
@@ -123,7 +125,7 @@ func RevokeInvite(c *m.ReqContext) Response {
return rsp
}
return ApiSuccess("Invite revoked")
return Success("Invite revoked")
}
func GetInviteInfoByCode(c *m.ReqContext) Response {
@@ -131,14 +133,14 @@ func GetInviteInfoByCode(c *m.ReqContext) Response {
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,
@@ -151,14 +153,14 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
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{
@@ -170,7 +172,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
}
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
@@ -189,14 +191,14 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
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
@@ -207,7 +209,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)
}
}
@@ -219,7 +221,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)
}
}

View File

@@ -20,13 +20,13 @@ func AddOrgUser(c *m.ReqContext, cmd m.AddOrgUserCommand) Response {
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
@@ -35,17 +35,17 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
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 *m.ReqContext) Response {
return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit"))
return getOrgUsersHelper(c.OrgId, c.Query("query"), c.QueryInt("limit"))
}
// GET /api/orgs/:orgId/users
@@ -53,22 +53,22 @@ func GetOrgUsers(c *m.ReqContext) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
}
func getOrgUsersHelper(orgId int64, query string, limit int) Response {
func getOrgUsersHelper(orgID int64, query string, limit int) Response {
q := m.GetOrgUsersQuery{
OrgId: orgId,
OrgId: orgID,
Query: query,
Limit: limit,
}
if err := bus.Dispatch(&q); err != nil {
return ApiError(500, "Failed to get account user", err)
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)
return JSON(200, q.Result)
}
// PATCH /api/org/users/:userId
@@ -87,41 +87,47 @@ func UpdateOrgUser(c *m.ReqContext, 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 *m.ReqContext) Response {
userId := c.ParamsInt64(":userId")
return removeOrgUserHelper(c.OrgId, userId)
return removeOrgUserHelper(&m.RemoveOrgUserCommand{
UserId: c.ParamsInt64(":userId"),
OrgId: c.OrgId,
ShouldDeleteOrphanedUser: true,
})
}
// DELETE /api/orgs/:orgId/users/:userId
func RemoveOrgUser(c *m.ReqContext) Response {
userId := c.ParamsInt64(":userId")
orgId := c.ParamsInt64(":orgId")
return removeOrgUserHelper(orgId, userId)
return removeOrgUserHelper(&m.RemoveOrgUserCommand{
UserId: c.ParamsInt64(":userId"),
OrgId: c.ParamsInt64(":orgId"),
})
}
func removeOrgUserHelper(orgId int64, userId int64) Response {
cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId}
if err := bus.Dispatch(&cmd); err != nil {
func removeOrgUserHelper(cmd *m.RemoveOrgUserCommand) Response {
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")
if cmd.UserWasDeleted {
return Success("User deleted")
}
return Success("User removed from organization")
}

View File

@@ -12,15 +12,15 @@ func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailFor
if err := bus.Dispatch(&userQuery); err != nil {
c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail)
return ApiError(200, "Email sent", err)
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 *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
@@ -28,13 +28,13 @@ func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
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 *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
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")
}

View File

@@ -33,7 +33,7 @@ func ValidateOrgPlaylist(c *m.ReqContext) {
return
}
if len(items) == 0 {
if len(items) == 0 && c.Context.Req.Method != "DELETE" {
c.JsonApiErr(404, "Playlist is empty", itemsErr)
return
}
@@ -55,10 +55,10 @@ func SearchPlaylists(c *m.ReqContext) 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 *m.ReqContext) Response {
@@ -66,7 +66,7 @@ func GetPlaylist(c *m.ReqContext) Response {
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)
@@ -79,7 +79,7 @@ func GetPlaylist(c *m.ReqContext) Response {
Items: playlistDTOs,
}
return Json(200, dto)
return JSON(200, dto)
}
func LoadPlaylistItemDTOs(id int64) ([]m.PlaylistItemDTO, error) {
@@ -120,21 +120,21 @@ func GetPlaylistItems(c *m.ReqContext) Response {
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 *m.ReqContext) Response {
playlistId := c.ParamsInt64(":id")
playlistID := c.ParamsInt64(":id")
playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, 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 *m.ReqContext) Response {
@@ -142,34 +142,35 @@ func DeletePlaylist(c *m.ReqContext) Response {
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 *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 *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
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)
}

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 int64, signedInUser *m.SignedInUser, 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},
SignedInUser: signedInUser,
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 int64, signedInUser *m.SignedInUser, dashboar
return result
}
func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, 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,9 +85,9 @@ func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistI
result := make(dtos.PlaylistDashboardsSlice, 0)
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
var k, _ = populateDashboardsByID(dashboardByIDs, dashboardIDOrder)
result = append(result, k...)
result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...)
result = append(result, populateDashboardsByTag(orgID, signedInUser, dashboardByTag, dashboardTagOrder)...)
sort.Sort(result)
return result, nil

View File

@@ -0,0 +1,171 @@
package pluginproxy
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"golang.org/x/oauth2/jwt"
)
var (
tokenCache = tokenCacheType{
cache: map[string]*jwtToken{},
}
oauthJwtTokenCache = oauthJwtTokenCacheType{
cache: map[string]*oauth2.Token{},
}
)
type tokenCacheType struct {
cache map[string]*jwtToken
sync.Mutex
}
type oauthJwtTokenCacheType struct {
cache map[string]*oauth2.Token
sync.Mutex
}
type accessTokenProvider struct {
route *plugins.AppPluginRoute
datasourceId int64
datasourceVersion int
}
type jwtToken struct {
ExpiresOn time.Time `json:"-"`
ExpiresOnString string `json:"expires_on"`
AccessToken string `json:"access_token"`
}
func newAccessTokenProvider(ds *models.DataSource, pluginRoute *plugins.AppPluginRoute) *accessTokenProvider {
return &accessTokenProvider{
datasourceId: ds.Id,
datasourceVersion: ds.Version,
route: pluginRoute,
}
}
func (provider *accessTokenProvider) getAccessToken(data templateData) (string, error) {
tokenCache.Lock()
defer tokenCache.Unlock()
if cachedToken, found := tokenCache.cache[provider.getAccessTokenCacheKey()]; found {
if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
logger.Info("Using token from cache")
return cachedToken.AccessToken, nil
}
}
urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, data)
if err != nil {
return "", err
}
params := make(url.Values)
for key, value := range provider.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.cache[provider.getAccessTokenCacheKey()] = &token
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
return token.AccessToken, nil
}
func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data templateData) (string, error) {
oauthJwtTokenCache.Lock()
defer oauthJwtTokenCache.Unlock()
if cachedToken, found := oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()]; found {
if cachedToken.Expiry.After(time.Now().Add(time.Second * 10)) {
logger.Debug("Using token from cache")
return cachedToken.AccessToken, nil
}
}
conf := &jwt.Config{}
if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok {
interpolatedVal, err := interpolateString(val, data)
if err != nil {
return "", err
}
conf.Email = interpolatedVal
}
if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok {
interpolatedVal, err := interpolateString(val, data)
if err != nil {
return "", err
}
conf.PrivateKey = []byte(interpolatedVal)
}
if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok {
interpolatedVal, err := interpolateString(val, data)
if err != nil {
return "", err
}
conf.TokenURL = interpolatedVal
}
conf.Scopes = provider.route.JwtTokenAuth.Scopes
token, err := getTokenSource(conf, ctx)
if err != nil {
return "", err
}
oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()] = token
logger.Info("Got new access token", "ExpiresOn", token.Expiry)
return token.AccessToken, nil
}
var getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
tokenSrc := conf.TokenSource(ctx)
token, err := tokenSrc.Token()
if err != nil {
return nil, err
}
return token, nil
}
func (provider *accessTokenProvider) getAccessTokenCacheKey() string {
return fmt.Sprintf("%v_%v_%v_%v", provider.datasourceId, provider.datasourceVersion, provider.route.Path, provider.route.Method)
}

View File

@@ -0,0 +1,94 @@
package pluginproxy
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
. "github.com/smartystreets/goconvey/convey"
"golang.org/x/oauth2"
"golang.org/x/oauth2/jwt"
)
func TestAccessToken(t *testing.T) {
Convey("Plugin with JWT token auth route", t, func() {
pluginRoute := &plugins.AppPluginRoute{
Path: "pathwithjwttoken1",
Url: "https://api.jwt.io/some/path",
Method: "GET",
JwtTokenAuth: &plugins.JwtTokenAuth{
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
Scopes: []string{
"https://www.testapi.com/auth/monitoring.read",
"https://www.testapi.com/auth/cloudplatformprojects.readonly",
},
Params: map[string]string{
"token_uri": "{{.JsonData.tokenUri}}",
"client_email": "{{.JsonData.clientEmail}}",
"private_key": "{{.SecureJsonData.privateKey}}",
},
},
}
templateData := templateData{
JsonData: map[string]interface{}{
"clientEmail": "test@test.com",
"tokenUri": "login.url.com/token",
},
SecureJsonData: map[string]string{
"privateKey": "testkey",
},
}
ds := &models.DataSource{Id: 1, Version: 2}
Convey("should fetch token using jwt private key", func() {
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{AccessToken: "abc"}, nil
}
provider := newAccessTokenProvider(ds, pluginRoute)
token, err := provider.getJwtAccessToken(context.Background(), templateData)
So(err, ShouldBeNil)
So(token, ShouldEqual, "abc")
})
Convey("should set jwt config values", func() {
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
So(conf.Email, ShouldEqual, "test@test.com")
So(conf.PrivateKey, ShouldResemble, []byte("testkey"))
So(len(conf.Scopes), ShouldEqual, 2)
So(conf.Scopes[0], ShouldEqual, "https://www.testapi.com/auth/monitoring.read")
So(conf.Scopes[1], ShouldEqual, "https://www.testapi.com/auth/cloudplatformprojects.readonly")
So(conf.TokenURL, ShouldEqual, "login.url.com/token")
return &oauth2.Token{AccessToken: "abc"}, nil
}
provider := newAccessTokenProvider(ds, pluginRoute)
_, err := provider.getJwtAccessToken(context.Background(), templateData)
So(err, ShouldBeNil)
})
Convey("should use cached token on second call", func() {
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: "abc",
Expiry: time.Now().Add(1 * time.Minute)}, nil
}
provider := newAccessTokenProvider(ds, pluginRoute)
token1, err := provider.getJwtAccessToken(context.Background(), templateData)
So(err, ShouldBeNil)
So(token1, ShouldEqual, "abc")
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{AccessToken: "error: cache not used"}, nil
}
token2, err := provider.getJwtAccessToken(context.Background(), templateData)
So(err, ShouldBeNil)
So(token2, ShouldEqual, "abc")
})
})
}

View File

@@ -0,0 +1,109 @@
package pluginproxy
import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"strings"
"text/template"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
"golang.org/x/oauth2/google"
)
//ApplyRoute should use the plugin route data to set auth headers and custom headers
func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.AppPluginRoute, ds *m.DataSource) {
proxyPath = strings.TrimPrefix(proxyPath, route.Path)
data := templateData{
JsonData: ds.JsonData.Interface().(map[string]interface{}),
SecureJsonData: ds.SecureJsonData.Decrypt(),
}
interpolatedURL, err := interpolateString(route.Url, data)
if err != nil {
logger.Error("Error interpolating proxy url", "error", err)
return
}
routeURL, err := url.Parse(interpolatedURL)
if err != nil {
logger.Error("Error parsing plugin route url", "error", err)
return
}
req.URL.Scheme = routeURL.Scheme
req.URL.Host = routeURL.Host
req.Host = routeURL.Host
req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxyPath)
if err := addHeaders(&req.Header, route, data); err != nil {
logger.Error("Failed to render plugin headers", "error", err)
}
tokenProvider := newAccessTokenProvider(ds, route)
if route.TokenAuth != nil {
if token, err := tokenProvider.getAccessToken(data); err != nil {
logger.Error("Failed to get access token", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt")
if route.JwtTokenAuth != nil && authenticationType == "jwt" {
if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
logger.Error("Failed to get access token", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
if authenticationType == "gce" {
tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...)
if err != nil {
logger.Error("Failed to get default token from meta data server", "error", err)
} else {
token, err := tokenSrc.Token()
if err != nil {
logger.Error("Failed to get default access token from meta data server", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
}
}
}
logger.Info("Requesting", "url", req.URL.String())
}
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
}

View File

@@ -0,0 +1,21 @@
package pluginproxy
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestDsAuthProvider(t *testing.T) {
Convey("When interpolating string", t, func() {
data := templateData{
SecureJsonData: map[string]string{
"Test": "0asd+asd",
},
}
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
So(err, ShouldBeNil)
So(interpolated, ShouldEqual, "0asd+asd")
})
}

View File

@@ -2,7 +2,6 @@ package pluginproxy
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
@@ -12,7 +11,6 @@ import (
"net/url"
"strconv"
"strings"
"text/template"
"time"
"github.com/opentracing/opentracing-go"
@@ -25,20 +23,10 @@ import (
)
var (
logger log.Logger = log.New("data-proxy-log")
client *http.Client = &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
tokenCache = map[int64]*jwtToken{}
logger = log.New("data-proxy-log")
client = newHTTPClient()
)
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
@@ -48,15 +36,26 @@ type DataSourceProxy struct {
plugin *plugins.DataSourcePlugin
}
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
targetUrl, _ := url.Parse(ds.Url)
targetURL, _ := url.Parse(ds.Url)
return &DataSourceProxy{
ds: ds,
plugin: plugin,
ctx: ctx,
proxyPath: proxyPath,
targetUrl: targetUrl,
targetUrl: targetURL,
}
}
func newHTTPClient() httpClient {
return &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
}
@@ -89,6 +88,9 @@ func (proxy *DataSourceProxy) HandleRequest() {
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,
@@ -98,6 +100,36 @@ func (proxy *DataSourceProxy) HandleRequest() {
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) useCustomHeaders(req *http.Request) {
decryptSdj := proxy.ds.SecureJsonData.Decrypt()
index := 1
for {
headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index)
headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index)
if key := proxy.ds.JsonData.Get(headerNameSuffix).MustString(); key != "" {
if val, ok := decryptSdj[headerValueSuffix]; ok {
// remove if exists
if req.Header.Get(key) != "" {
req.Header.Del(key)
}
req.Header.Add(key, val)
logger.Debug("Using custom header ", "CustomHeaders", key)
}
} else {
break
}
index += 1
}
}
func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
return func(req *http.Request) {
req.URL.Scheme = proxy.targetUrl.Scheme
@@ -121,12 +153,16 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
} 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))
}
// Lookup and use custom headers
if proxy.ds.SecureJsonData != nil {
proxy.useCustomHeaders(req)
}
dsAuth := req.Header.Get("X-DS-Authorization")
if len(dsAuth) > 0 {
req.Header.Del("X-DS-Authorization")
@@ -157,6 +193,11 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Forwarded-Proto")
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
// Clear Origin and Referer to avoir CORS issues
req.Header.Del("Origin")
req.Header.Del("Referer")
// set X-Forwarded-For header
if req.RemoteAddr != "" {
@@ -172,18 +213,12 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
}
if proxy.route != nil {
proxy.applyRoute(req)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
}
}
}
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")
}
@@ -270,110 +305,3 @@ func checkWhiteList(c *m.ReqContext, host string) bool {
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 {
if interpolatedParam, err := interpolateString(value, data); err != nil {
return "", err
} else {
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 "", errors.New(fmt.Sprintf("Could not parse template %s.", text))
}
var contentBuf bytes.Buffer
err = t.Execute(&contentBuf, data)
if err != nil {
return "", errors.New(fmt.Sprintf("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
}

View File

@@ -1,13 +1,18 @@
package pluginproxy
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
@@ -44,6 +49,13 @@ func TestDSRouteRule(t *testing.T) {
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
{
Path: "api/common",
Url: "{{.JsonData.dynamicUrl}}",
Headers: []plugins.AppPluginRouteHeader{
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
},
}
@@ -52,7 +64,8 @@ func TestDSRouteRule(t *testing.T) {
ds := &m.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"clientId": "asd",
"clientId": "asd",
"dynamicUrl": "https://dynamic.grafana.com",
}),
SecureJsonData: map[string][]byte{
"key": key,
@@ -70,7 +83,7 @@ func TestDSRouteRule(t *testing.T) {
Convey("When matching route path", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
proxy.route = plugin.Routes[0]
proxy.applyRoute(req)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
Convey("should add headers and update url", func() {
So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
@@ -78,6 +91,17 @@ func TestDSRouteRule(t *testing.T) {
})
})
Convey("When matching route path and has dynamic url", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method")
proxy.route = plugin.Routes[3]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
Convey("should add headers and interpolate the url", func() {
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.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")
@@ -100,21 +124,128 @@ func TestDSRouteRule(t *testing.T) {
})
})
Convey("Plugin with multiple routes for token auth", func() {
plugin := &plugins.DataSourcePlugin{
Routes: []*plugins.AppPluginRoute{
{
Path: "pathwithtoken1",
Url: "https://api.nr1.io/some/path",
TokenAuth: &plugins.JwtTokenAuth{
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
Params: map[string]string{
"grant_type": "client_credentials",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://api.nr1.io",
},
},
},
{
Path: "pathwithtoken2",
Url: "https://api.nr2.io/some/path",
TokenAuth: &plugins.JwtTokenAuth{
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
Params: map[string]string{
"grant_type": "client_credentials",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://api.nr2.io",
},
},
},
},
}
setting.SecretKey = "password"
key, _ := util.Encrypt([]byte("123"), "password")
ds := &m.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"clientId": "asd",
"tenantId": "mytenantId",
}),
SecureJsonData: map[string][]byte{
"clientSecret": 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 creating and caching access tokens", func() {
var authorizationHeaderCall1 string
var authorizationHeaderCall2 string
Convey("first call should add authorization header with access token", func() {
json, err := ioutil.ReadFile("./test-data/access-token-1.json")
So(err, ShouldBeNil)
client = newFakeHTTPClient(json)
proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
proxy1.route = plugin.Routes[0]
ApplyRoute(proxy1.ctx.Req.Context(), req, proxy1.proxyPath, proxy1.route, proxy1.ds)
authorizationHeaderCall1 = req.Header.Get("Authorization")
So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
Convey("second call to another route should add a different access token", func() {
json2, err := ioutil.ReadFile("./test-data/access-token-2.json")
So(err, ShouldBeNil)
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
client = newFakeHTTPClient(json2)
proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2")
proxy2.route = plugin.Routes[1]
ApplyRoute(proxy2.ctx.Req.Context(), req, proxy2.proxyPath, proxy2.route, proxy2.ds)
authorizationHeaderCall2 = req.Header.Get("Authorization")
So(req.URL.String(), ShouldEqual, "https://api.nr2.io/some/path")
So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall2, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall2, ShouldNotEqual, authorizationHeaderCall1)
Convey("third call to first route should add cached access token", func() {
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
client = newFakeHTTPClient([]byte{})
proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
proxy3.route = plugin.Routes[0]
ApplyRoute(proxy3.ctx.Req.Context(), req, proxy3.proxyPath, proxy3.route, proxy3.ds)
authorizationHeaderCall3 := req.Header.Get("Authorization")
So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall3, ShouldStartWith, "Bearer eyJ0e")
So(authorizationHeaderCall3, ShouldEqual, authorizationHeaderCall1)
})
})
})
})
})
Convey("When proxying graphite", func() {
setting.BuildVersion = "5.3.0"
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
So(err, ShouldBeNil)
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
proxy.getDirector()(&req)
proxy.getDirector()(req)
Convey("Can translate request url and path", func() {
So(req.URL.Host, ShouldEqual, "graphite:8080")
So(req.URL.Path, ShouldEqual, "/render")
So(req.Header.Get("User-Agent"), ShouldEqual, "Grafana/5.3.0")
})
})
@@ -132,10 +263,10 @@ func TestDSRouteRule(t *testing.T) {
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
So(err, ShouldBeNil)
proxy.getDirector()(&req)
proxy.getDirector()(req)
Convey("Should add db to url", func() {
So(req.URL.Path, ShouldEqual, "/db/site/")
@@ -162,8 +293,8 @@ func TestDSRouteRule(t *testing.T) {
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl, Header: make(http.Header)}
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)
@@ -188,8 +319,8 @@ func TestDSRouteRule(t *testing.T) {
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl, Header: make(http.Header)}
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)
@@ -200,17 +331,86 @@ func TestDSRouteRule(t *testing.T) {
})
})
Convey("When interpolating string", func() {
data := templateData{
SecureJsonData: map[string]string{
"Test": "0asd+asd",
Convey("When proxying a data source with custom headers specified", func() {
plugin := &plugins.DataSourcePlugin{}
encryptedData, err := util.Encrypt([]byte(`Bearer xf5yhfkpsnmgo`), setting.SecretKey)
ds := &m.DataSource{
Type: m.DS_PROMETHEUS,
Url: "http://prometheus:9090",
JsonData: simplejson.NewFromAny(map[string]interface{}{
"httpHeaderName1": "Authorization",
}),
SecureJsonData: map[string][]byte{
"httpHeaderValue1": encryptedData,
},
}
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
So(err, ShouldBeNil)
So(interpolated, ShouldEqual, "0asd+asd")
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestURL, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestURL, Header: make(http.Header)}
proxy.getDirector()(&req)
if err != nil {
log.Fatal(4, err.Error())
}
Convey("Match header value after decryption", func() {
So(req.Header.Get("Authorization"), ShouldEqual, "Bearer xf5yhfkpsnmgo")
})
})
Convey("When proxying a custom datasource", func() {
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{
Type: "custom-datasource",
Url: "http://host/root/",
}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/")
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
req.Header.Add("Origin", "grafana.com")
req.Header.Add("Referer", "grafana.com")
req.Header.Add("X-Canary", "stillthere")
So(err, ShouldBeNil)
proxy.getDirector()(req)
Convey("Should keep user request (including trailing slash)", func() {
So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/")
})
Convey("Origin and Referer headers should be dropped", func() {
So(req.Header.Get("Origin"), ShouldEqual, "")
So(req.Header.Get("Referer"), ShouldEqual, "")
So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere")
})
})
})
}
type httpClientStub struct {
fakeBody []byte
}
func (c *httpClientStub) Do(req *http.Request) (*http.Response, error) {
bodyJSON, _ := simplejson.NewJson(c.fakeBody)
_, passedTokenCacheTest := bodyJSON.CheckGet("expires_on")
So(passedTokenCacheTest, ShouldBeTrue)
bodyJSON.Set("expires_on", fmt.Sprint(time.Now().Add(time.Second*60).Unix()))
body, _ := bodyJSON.MarshalJSON()
resp := &http.Response{
Body: ioutil.NopCloser(bytes.NewReader(body)),
}
return resp, nil
}
func newFakeHTTPClient(fakeBody []byte) httpClient {
return &httpClientStub{
fakeBody: fakeBody,
}
}

View File

@@ -19,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,16 +37,16 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.
return result, err
}
func NewApiPluginProxy(ctx *m.ReqContext, 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")
@@ -80,7 +80,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
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

View File

@@ -0,0 +1,9 @@
{
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "0",
"expires_on": "1528740417",
"not_before": "1528736517",
"resource": "https://api.nr1.io",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayIsImtpZCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayJ9.eyJhdWQiOiJodHRwczovL2FwaS5sb2dhbmFseXRpY3MuaW8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9lN2YzZjY2MS1hOTMzLTRiM2YtODE3Ni01MWM0Zjk4MmVjNDgvIiwiaWF0IjoxNTI4NzM2NTE3LCJuYmYiOjE1Mjg3MzY1MTcsImV4cCI6MTUyODc0MDQxNywiYWlvIjoiWTJkZ1lBaStzaWRsT3NmQ2JicGhLMSsremttN0NBQT0iLCJhcHBpZCI6IjdmMzJkYjdjLTZmNmYtNGU4OC05M2Q5LTlhZTEyNmMwYTU1ZiIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2U3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OC8iLCJvaWQiOiI1NDQ5ZmJjOS1mYWJhLTRkNjItODE2Yy05ZmMwMzZkMWViN2UiLCJzdWIiOiI1NDQ5ZmJjOS1mYWJhLTRkNjItODE2Yy05ZmMwMzZkMWViN2UiLCJ0aWQiOiJlN2YzZjY2MS1hOTMzLTRiM2YtODE3Ni01MWM0Zjk4MmVjNDgiLCJ1dGkiOiJZQTlQa2lxUy1VV1hMQjhIRnU0U0FBIiwidmVyIjoiMS4wIn0.ga5qudt4LDMKTStAxUmzjyZH8UFBAaFirJqpTdmYny4NtkH6JT2EILvjTjYxlKeTQisvwx9gof0PyicZIab9d6wlMa2xiLzr2nmaOonYClY8fqBaRTgc1xVjrKFw5SCgpx3FnEyJhIWvVPIfaWaogSHcQbIpe4kdk4tz-ccmrx0D1jsziSI4BZcJcX04aJuHZGz9k4mQZ_AA5sQSeQaNuojIng6rYoIifAXFYBZPTbeeeqmiGq8v0IOLeNKbC0POeQCJC_KKBG6Z_MV2KgPxFEzQuX2ZFmRD_wGPteV5TUBxh1kARdqexA3e0zAKSawR9kmrAiZ21lPr4tX2Br_HDg"
}

View File

@@ -0,0 +1,9 @@
{
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "0",
"expires_on": "1528662059",
"not_before": "1528658159",
"resource": "https://api.nr2.io",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayIsImtpZCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2U3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OC8iLCJpYXQiOjE1Mjg2NTgxNTksIm5iZiI6MTUyODY1ODE1OSwiZXhwIjoxNTI4NjYyMDU5LCJhaW8iOiJZMmRnWUFpK3NpZGxPc2ZDYmJwaEsxKyt6a203Q0FBPSIsImFwcGlkIjoiODg5YjdlZDgtMWFlZC00ODZlLTk3ODktODE5NzcwYmJiNjFhIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZTdmM2Y2NjEtYTkzMy00YjNmLTgxNzYtNTFjNGY5ODJlYzQ4LyIsIm9pZCI6IjY0YzQxNjMyLTliOWUtNDczNy05MTYwLTBlNjAzZTg3NjljYyIsInN1YiI6IjY0YzQxNjMyLTliOWUtNDczNy05MTYwLTBlNjAzZTg3NjljYyIsInRpZCI6ImU3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OCIsInV0aSI6IkQ1ODZHSGUySDBPd0ptOU0xeVlKQUEiLCJ2ZXIiOiIxLjAifQ.Pw8c8gpoZptw3lGreQoHQaMVOozSaTE5D38Vm2aCHRB3DvD3N-Qcm1x0ZCakUEV2sJd7jvx4XtPFuW7063T0V1deExL4rzzvIo0ZfMmURf9tCTiKFKYibqf8_PtfPSz0t9eNDEUGmWDh1Wgssb4W_H-wPqgl9VPMT7T6ynkfIm0-ODPZTBzgSHiY8C_L1-DkhsK7XiqbUlSDgx9FpfChZS3ah8QhA8geqnb_HVuSktg7WhpxmogSpK5QdrwSE3jsbItpzOfLJ4iBd2ExzS2C0y8H_Coluk3Y1YA07tAxJ6Y7oBv-XwGqNfZhveOCQOzX-U3dFod3fXXysjB0UB89WQ"
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func GetPluginList(c *m.ReqContext) Response {
func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
embeddedFilter := c.Query("embedded")
@@ -19,7 +19,7 @@ func GetPluginList(c *m.ReqContext) 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)
@@ -39,6 +39,10 @@ func GetPluginList(c *m.ReqContext) Response {
continue
}
if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels {
continue
}
listItem := dtos.PluginListItem{
Id: pluginDef.Id,
Name: pluginDef.Name,
@@ -75,92 +79,94 @@ func GetPluginList(c *m.ReqContext) Response {
}
sort.Sort(result)
return Json(200, result)
return JSON(200, result)
}
func GetPluginSettingById(c *m.ReqContext) 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,
State: def.State,
}
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 *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response {
pluginId := c.Params(":pluginId")
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 *m.ReqContext) Response {
pluginId := c.Params(":pluginId")
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 GetPluginMarkdown(c *m.ReqContext) Response {
pluginId := c.Params(":pluginId")
pluginID := c.Params(":pluginId")
name := c.Params(":name")
if content, err := plugins.GetPluginMarkdown(pluginId, name); 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 markdown file", err)
} else {
resp := Respond(200, content)
resp.Header("Content-Type", "text/plain; charset=utf-8")
return resp
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 *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response {
@@ -172,12 +178,13 @@ func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Respon
Path: apiCmd.Path,
Inputs: apiCmd.Inputs,
Overwrite: apiCmd.Overwrite,
FolderId: apiCmd.FolderId,
Dashboard: apiCmd.Dashboard,
}
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)
}

View File

@@ -13,60 +13,61 @@ func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response {
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 *m.ReqContext) Response {
return getPreferencesFor(c.OrgId, c.UserId)
return getPreferencesFor(c.OrgId, c.UserId, 0)
}
func getPreferencesFor(orgId int64, userId int64) Response {
prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId}
func getPreferencesFor(orgID, userID, teamID int64) Response {
prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID, TeamId: teamID}
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 *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
return updatePreferencesFor(c.OrgId, c.UserId, 0, &dtoCmd)
}
func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
func updatePreferencesFor(orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
saveCmd := m.SavePreferencesCommand{
UserId: userId,
OrgId: orgId,
UserId: userID,
OrgId: orgID,
TeamId: teamId,
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 *m.ReqContext) Response {
return getPreferencesFor(c.OrgId, 0)
return getPreferencesFor(c.OrgId, 0, 0)
}
// PUT /api/org/preferences
func UpdateOrgPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
return updatePreferencesFor(c.OrgId, 0, &dtoCmd)
return updatePreferencesFor(c.OrgId, 0, 0, &dtoCmd)
}

View File

@@ -8,60 +8,60 @@ import (
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 *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 *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 *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")
}

View File

@@ -3,44 +3,75 @@ package api
import (
"fmt"
"net/http"
"runtime"
"strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/renderer"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/util"
)
func RenderToPng(c *m.ReqContext) {
func (hs *HTTPServer) 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,
UserId: c.UserId,
OrgRole: c.OrgRole,
Timezone: queryReader.Get("tz", ""),
Encoding: queryReader.Get("encoding", ""),
width, err := strconv.Atoi(queryReader.Get("width", "800"))
if err != nil {
c.Handle(400, "Render parameters error", fmt.Errorf("Cannot parse width as int: %s", err))
return
}
pngPath, err := renderer.RenderToPng(renderOpts)
height, err := strconv.Atoi(queryReader.Get("height", "400"))
if err != nil {
c.Handle(400, "Render parameters error", fmt.Errorf("Cannot parse height as int: %s", err))
return
}
if err != nil && err == renderer.ErrTimeout {
timeout, err := strconv.Atoi(queryReader.Get("timeout", "60"))
if err != nil {
c.Handle(400, "Render parameters error", fmt.Errorf("Cannot parse timeout as int: %s", err))
return
}
result, err := hs.RenderService.Render(c.Req.Context(), rendering.Opts{
Width: width,
Height: height,
Timeout: time.Duration(timeout) * time.Second,
OrgId: c.OrgId,
UserId: c.UserId,
OrgRole: c.OrgRole,
Path: c.Params("*") + queryParams,
Timezone: queryReader.Get("tz", ""),
Encoding: queryReader.Get("encoding", ""),
ConcurrentLimit: 30,
})
if err != nil && err == rendering.ErrTimeout {
c.Handle(500, err.Error(), err)
return
}
if err != nil && err == rendering.ErrPhantomJSNotInstalled {
if strings.HasPrefix(runtime.GOARCH, "arm") {
c.Handle(500, "Rendering failed - PhantomJS isn't included in arm build per default", err)
} else {
c.Handle(500, "Rendering failed - PhantomJS isn't installed correctly", err)
}
return
}
if err != nil {
c.Handle(500, "Rendering failed.", err)
return
}
c.Resp.Header().Set("Content-Type", "image/png")
http.ServeFile(c.Resp, c.Req.Request, pngPath)
http.ServeFile(c.Resp, c.Req.Request, result.FilePath)
}

View File

@@ -1,9 +1,10 @@
package api
package routing
import (
"net/http"
"strings"
macaron "gopkg.in/macaron.v1"
"gopkg.in/macaron.v1"
)
type Router interface {
@@ -11,22 +12,43 @@ type Router interface {
Get(pattern string, handlers ...macaron.Handler) *macaron.Route
}
// RouteRegister allows you to add routes and macaron.Handlers
// that the web server should serve.
type RouteRegister interface {
// Get adds a list of handlers to a given route with a GET HTTP verb
Get(string, ...macaron.Handler)
// Post adds a list of handlers to a given route with a POST HTTP verb
Post(string, ...macaron.Handler)
// Delete adds a list of handlers to a given route with a DELETE HTTP verb
Delete(string, ...macaron.Handler)
// Put adds a list of handlers to a given route with a PUT HTTP verb
Put(string, ...macaron.Handler)
// Patch adds a list of handlers to a given route with a PATCH HTTP verb
Patch(string, ...macaron.Handler)
// Any adds a list of handlers to a given route with any HTTP verb
Any(string, ...macaron.Handler)
// Group allows you to pass a function that can add multiple routes
// with a shared prefix route.
Group(string, func(RouteRegister), ...macaron.Handler)
Register(Router) *macaron.Router
// Insert adds more routes to an existing Group.
Insert(string, func(RouteRegister), ...macaron.Handler)
// Register iterates over all routes added to the RouteRegister
// and add them to the `Router` pass as an parameter.
Register(Router)
}
type RegisterNamedMiddleware func(name string) macaron.Handler
func newRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
func NewRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
return &routeRegister{
prefix: "",
routes: []route{},
@@ -49,6 +71,24 @@ type routeRegister struct {
groups []*routeRegister
}
func (rr *routeRegister) Insert(pattern string, fn func(RouteRegister), handlers ...macaron.Handler) {
//loop over all groups at current level
for _, g := range rr.groups {
// apply routes if the prefix matches the pattern
if g.prefix == pattern {
g.Group("", fn)
break
}
// go down one level if the prefix can be find in the pattern
if strings.HasPrefix(pattern, g.prefix) {
g.Insert(pattern, fn)
}
}
}
func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handlers ...macaron.Handler) {
group := &routeRegister{
prefix: rr.prefix + pattern,
@@ -61,7 +101,7 @@ func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handle
rr.groups = append(rr.groups, group)
}
func (rr *routeRegister) Register(router Router) *macaron.Router {
func (rr *routeRegister) Register(router Router) {
for _, r := range rr.routes {
// GET requests have to be added to macaron routing using Get()
// Otherwise HEAD requests will not be allowed.
@@ -76,8 +116,6 @@ func (rr *routeRegister) Register(router Router) *macaron.Router {
for _, g := range rr.groups {
g.Register(router)
}
return &macaron.Router{}
}
func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {
@@ -89,6 +127,12 @@ func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handl
h = append(h, rr.subfixHandlers...)
h = append(h, handlers...)
for _, r := range rr.routes {
if r.pattern == rr.prefix+pattern && r.method == method {
panic("cannot add duplicate route")
}
}
rr.routes = append(rr.routes, route{
method: method,
pattern: rr.prefix + pattern,

View File

@@ -1,11 +1,11 @@
package api
package routing
import (
"net/http"
"strconv"
"testing"
macaron "gopkg.in/macaron.v1"
"gopkg.in/macaron.v1"
)
type fakeRouter struct {
@@ -33,7 +33,7 @@ func (fr *fakeRouter) Get(pattern string, handlers ...macaron.Handler) *macaron.
}
func emptyHandlers(n int) []macaron.Handler {
res := []macaron.Handler{}
var res []macaron.Handler
for i := 1; n >= i; i++ {
res = append(res, emptyHandler(strconv.Itoa(i)))
}
@@ -51,7 +51,7 @@ func TestRouteSimpleRegister(t *testing.T) {
}
// Setup
rr := newRouteRegister(func(name string) macaron.Handler {
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
@@ -96,7 +96,7 @@ func TestRouteGroupedRegister(t *testing.T) {
}
// Setup
rr := newRouteRegister()
rr := NewRouteRegister()
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
@@ -138,7 +138,78 @@ func TestRouteGroupedRegister(t *testing.T) {
}
}
}
func TestRouteGroupInserting(t *testing.T) {
testTable := []route{
{method: http.MethodGet, pattern: "/api/", handlers: emptyHandlers(1)},
{method: http.MethodPost, pattern: "/api/group/endpoint", handlers: emptyHandlers(1)},
{method: http.MethodGet, pattern: "/api/group/inserted", handlers: emptyHandlers(1)},
{method: http.MethodDelete, pattern: "/api/inserted-endpoint", handlers: emptyHandlers(1)},
}
// Setup
rr := NewRouteRegister()
rr.Group("/api", func(api RouteRegister) {
api.Get("/", emptyHandler("1"))
api.Group("/group", func(group RouteRegister) {
group.Post("/endpoint", emptyHandler("1"))
})
})
rr.Insert("/api", func(api RouteRegister) {
api.Delete("/inserted-endpoint", emptyHandler("1"))
})
rr.Insert("/api/group", func(group RouteRegister) {
group.Get("/inserted", emptyHandler("1"))
})
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Fatalf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
func TestDuplicateRoutShouldPanic(t *testing.T) {
defer func() {
if recover() != "cannot add duplicate route" {
t.Errorf("Should cause panic if duplicate routes are added ")
}
}()
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
rr.Get("/api", emptyHandler("1"))
rr.Get("/api", emptyHandler("1"))
fr := &fakeRouter{}
rr.Register(fr)
}
func TestNamedMiddlewareRouteRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(2)},
@@ -150,7 +221,7 @@ func TestNamedMiddlewareRouteRegister(t *testing.T) {
}
// Setup
rr := newRouteRegister(func(name string) macaron.Handler {
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})

View File

@@ -25,19 +25,19 @@ func Search(c *m.ReqContext) {
permission = m.PERMISSION_EDIT
}
dbids := make([]int64, 0)
dbIDs := make([]int64, 0)
for _, id := range c.QueryStrings("dashboardIds") {
dashboardId, err := strconv.ParseInt(id, 10, 64)
dashboardID, err := strconv.ParseInt(id, 10, 64)
if err == nil {
dbids = append(dbids, dashboardId)
dbIDs = append(dbIDs, dashboardID)
}
}
folderIds := make([]int64, 0)
folderIDs := make([]int64, 0)
for _, id := range c.QueryStrings("folderIds") {
folderId, err := strconv.ParseInt(id, 10, 64)
folderID, err := strconv.ParseInt(id, 10, 64)
if err == nil {
folderIds = append(folderIds, folderId)
folderIDs = append(folderIDs, folderID)
}
}
@@ -48,9 +48,9 @@ func Search(c *m.ReqContext) {
Limit: limit,
IsStarred: starred == "true",
OrgId: c.OrgId,
DashboardIds: dbids,
DashboardIds: dbIDs,
Type: dashboardType,
FolderIds: folderIds,
FolderIds: folderIDs,
Permission: permission,
}

View File

@@ -12,7 +12,7 @@ import (
// GET /api/user/signup/options
func GetSignUpOptions(c *m.ReqContext) Response {
return Json(200, util.DynMap{
return JSON(200, util.DynMap{
"verifyEmailEnabled": setting.VerifyEmailEnabled,
"autoAssignOrg": setting.AutoAssignOrg,
})
@@ -21,12 +21,12 @@ func GetSignUpOptions(c *m.ReqContext) Response {
// POST /api/user/signup
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{}
@@ -38,7 +38,7 @@ func SignUp(c *m.ReqContext, 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{
@@ -48,12 +48,12 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
metrics.M_Api_User_SignUpStarted.Inc()
return Json(200, util.DynMap{"status": "SignUpCreated"})
return JSON(200, util.DynMap{"status": "SignUpCreated"})
}
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{
@@ -75,12 +75,12 @@ func SignUpStep2(c *m.ReqContext, 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
@@ -98,7 +98,7 @@ func SignUpStep2(c *m.ReqContext, 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"}
@@ -112,7 +112,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
loginUserWithUser(user, c)
metrics.M_Api_User_SignUpCompleted.Inc()
return Json(200, apiResponse)
return JSON(200, apiResponse)
}
func verifyUserSignUpEmail(email string, code string) (bool, Response) {
@@ -120,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

View File

@@ -7,20 +7,20 @@ import (
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 *m.ReqContext) Response {
@@ -28,12 +28,12 @@ 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")
}

View File

@@ -48,7 +48,7 @@ type StaticOptions struct {
// Expires defines which user-defined function to use for producing a HTTP Expires Header
// https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
AddHeaders func(ctx *macaron.Context)
// FileSystem is the interface for supporting any implmentation of file system.
// FileSystem is the interface for supporting any implementation of file system.
FileSystem http.FileSystem
}

View File

@@ -12,12 +12,12 @@ func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
return ApiError(409, "Team name taken", err)
return Error(409, "Team name taken", err)
}
return ApiError(500, "Failed to create Team", err)
return Error(500, "Failed to create Team", err)
}
return Json(200, &util.DynMap{
return JSON(200, &util.DynMap{
"teamId": cmd.Result.Id,
"message": "Team created",
})
@@ -29,23 +29,23 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
cmd.Id = c.ParamsInt64(":teamId")
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNameTaken {
return ApiError(400, "Team name taken", err)
return Error(400, "Team name taken", err)
}
return ApiError(500, "Failed to update Team", err)
return Error(500, "Failed to update Team", err)
}
return ApiSuccess("Team updated")
return Success("Team updated")
}
// DELETE /api/teams/:teamId
func DeleteTeamById(c *m.ReqContext) Response {
func DeleteTeamByID(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Failed to delete Team. ID not found", nil)
return Error(404, "Failed to delete Team. ID not found", nil)
}
return ApiError(500, "Failed to update Team", err)
return Error(500, "Failed to update Team", err)
}
return ApiSuccess("Team deleted")
return Success("Team deleted")
}
// GET /api/teams/search
@@ -68,7 +68,7 @@ func SearchTeams(c *m.ReqContext) Response {
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search Teams", err)
return Error(500, "Failed to search Teams", err)
}
for _, team := range query.Result.Teams {
@@ -78,20 +78,31 @@ func SearchTeams(c *m.ReqContext) Response {
query.Result.Page = page
query.Result.PerPage = perPage
return Json(200, query.Result)
return JSON(200, query.Result)
}
// GET /api/teams/:teamId
func GetTeamById(c *m.ReqContext) Response {
func GetTeamByID(c *m.ReqContext) Response {
query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Team not found", err)
return Error(404, "Team not found", err)
}
return ApiError(500, "Failed to get Team", err)
return Error(500, "Failed to get Team", err)
}
return Json(200, &query.Result)
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
return JSON(200, &query.Result)
}
// GET /api/teams/:teamId/preferences
func GetTeamPreferences(c *m.ReqContext) Response {
return getPreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"))
}
// PUT /api/teams/:teamId/preferences
func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
return updatePreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"), &dtoCmd)
}

View File

@@ -4,6 +4,7 @@ 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/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -12,14 +13,19 @@ func GetTeamMembers(c *m.ReqContext) Response {
query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get Team Members", err)
return Error(500, "Failed to get Team Members", err)
}
for _, member := range query.Result {
member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
member.Labels = []string{}
if setting.IsEnterprise && setting.LdapEnabled && member.External {
member.Labels = append(member.Labels, "LDAP")
}
}
return Json(200, query.Result)
return JSON(200, query.Result)
}
// POST /api/teams/:teamId/members
@@ -29,17 +35,17 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Team not found", nil)
return Error(404, "Team not found", nil)
}
if err == m.ErrTeamMemberAlreadyAdded {
return ApiError(400, "User is already added to this team", nil)
return Error(400, "User is already added to this team", nil)
}
return ApiError(500, "Failed to add Member to Team", err)
return Error(500, "Failed to add Member to Team", err)
}
return Json(200, &util.DynMap{
return JSON(200, &util.DynMap{
"message": "Member added to Team",
})
}
@@ -48,14 +54,14 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
func RemoveTeamMember(c *m.ReqContext) Response {
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
if err == m.ErrTeamNotFound {
return ApiError(404, "Team not found", nil)
return Error(404, "Team not found", nil)
}
if err == m.ErrTeamMemberNotFound {
return ApiError(404, "Team member not found", nil)
return Error(404, "Team member not found", nil)
}
return ApiError(500, "Failed to remove Member from Team", err)
return Error(500, "Failed to remove Member from Team", err)
}
return ApiSuccess("Team Member removed")
return Success("Team Member removed")
}

View File

@@ -13,7 +13,7 @@ import (
func TestTeamApiEndpoint(t *testing.T) {
Convey("Given two teams", t, func() {
mockResult := models.SearchTeamQueryResult{
Teams: []*models.SearchTeamDto{
Teams: []*models.TeamDTO{
{Name: "team1"},
{Name: "team2"},
},

View File

@@ -14,21 +14,21 @@ func GetSignedInUser(c *m.ReqContext) Response {
}
// GET /api/users/:id
func GetUserById(c *m.ReqContext) 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
@@ -36,9 +36,9 @@ 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{
@@ -50,17 +50,17 @@ func GetUserByLoginOrEmail(c *m.ReqContext) Response {
IsGrafanaAdmin: user.IsAdmin,
OrgId: user.OrgId,
}
return Json(200, &result)
return JSON(200, &result)
}
// POST /api/user
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
@@ -75,35 +75,35 @@ func UpdateUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response {
//POST /api/users/:id/using/:orgId
func UpdateUserActiveOrg(c *m.ReqContext) Response {
userId := c.ParamsInt64(":id")
orgId := c.ParamsInt64(":orgId")
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
@@ -111,23 +111,38 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
return getUserOrgList(c.UserId)
}
// GET /api/user/teams
func GetSignedInUserTeamList(c *m.ReqContext) Response {
query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
if err := bus.Dispatch(&query); err != nil {
return Error(500, "Failed to get user teams", err)
}
for _, team := range query.Result {
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
}
return JSON(200, query.Result)
}
// GET /api/user/:id/orgs
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
@@ -136,7 +151,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
}
}
@@ -146,33 +161,33 @@ func validateUsingOrg(userId int64, orgId int64) bool {
// POST /api/user/using/:id
func UserSetUsingOrg(c *m.ReqContext) Response {
orgId := c.ParamsInt64(":id")
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 *m.ReqContext) {
orgId := c.ParamsInt64(":id")
func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
orgID := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) {
NotFoundHandler(c)
if !validateUsingOrg(c.UserId, orgID) {
hs.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)
hs.NotFoundHandler(c)
}
c.Redirect(setting.AppSubUrl + "/")
@@ -180,53 +195,53 @@ func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
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 *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/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 *m.ReqContext) (*m.SearchUsersQuery, error) {
@@ -269,10 +284,10 @@ func SetHelpFlag(c *m.ReqContext) 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 *m.ReqContext) Response {
@@ -282,8 +297,8 @@ func ClearHelpFlags(c *m.ReqContext) 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})
}

View File

@@ -2,7 +2,7 @@ package bus
import (
"context"
"fmt"
"errors"
"reflect"
)
@@ -10,21 +10,44 @@ type HandlerFunc interface{}
type CtxHandlerFunc func()
type Msg interface{}
var ErrHandlerNotFound = errors.New("handler not found")
type TransactionManager interface {
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}
type Bus interface {
Dispatch(msg Msg) error
DispatchCtx(ctx context.Context, msg Msg) error
Publish(msg Msg) error
// InTransaction starts a transaction and store it in the context.
// The caller can then pass a function with multiple DispatchCtx calls that
// all will be executed in the same transaction. InTransaction will rollback if the
// callback returns an error.
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
AddHandler(handler HandlerFunc)
AddCtxHandler(handler HandlerFunc)
AddHandlerCtx(handler HandlerFunc)
AddEventListener(handler HandlerFunc)
AddWildcardListener(handler HandlerFunc)
// SetTransactionManager allows the user to replace the internal
// noop TransactionManager that is responsible for manageing
// transactions in `InTransaction`
SetTransactionManager(tm TransactionManager)
}
func (b *InProcBus) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return b.txMng.InTransaction(ctx, fn)
}
type InProcBus struct {
handlers map[string]HandlerFunc
handlersWithCtx map[string]HandlerFunc
listeners map[string][]HandlerFunc
wildcardListeners []HandlerFunc
txMng TransactionManager
}
// temp stuff, not sure how to handle bus instance, and init yet
@@ -33,50 +56,70 @@ var globalBus = New()
func New() Bus {
bus := &InProcBus{}
bus.handlers = make(map[string]HandlerFunc)
bus.handlersWithCtx = make(map[string]HandlerFunc)
bus.listeners = make(map[string][]HandlerFunc)
bus.wildcardListeners = make([]HandlerFunc, 0)
bus.txMng = &noopTransactionManager{}
return bus
}
// Want to get rid of global bus
func GetBus() Bus {
return globalBus
}
func (b *InProcBus) SetTransactionManager(tm TransactionManager) {
b.txMng = tm
}
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var handler = b.handlers[msgName]
var handler = b.handlersWithCtx[msgName]
if handler == nil {
return fmt.Errorf("handler not found for %s", msgName)
return ErrHandlerNotFound
}
var params = make([]reflect.Value, 2)
params[0] = reflect.ValueOf(ctx)
params[1] = reflect.ValueOf(msg)
var params = []reflect.Value{}
params = append(params, reflect.ValueOf(ctx))
params = append(params, reflect.ValueOf(msg))
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
} else {
return err.(error)
}
return err.(error)
}
func (b *InProcBus) Dispatch(msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var handler = b.handlers[msgName]
var handler = b.handlersWithCtx[msgName]
withCtx := true
if handler == nil {
return fmt.Errorf("handler not found for %s", msgName)
withCtx = false
handler = b.handlers[msgName]
}
var params = make([]reflect.Value, 1)
params[0] = reflect.ValueOf(msg)
if handler == nil {
return ErrHandlerNotFound
}
var params = []reflect.Value{}
if withCtx {
params = append(params, reflect.ValueOf(context.Background()))
}
params = append(params, reflect.ValueOf(msg))
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
} else {
return err.(error)
}
return err.(error)
}
func (b *InProcBus) Publish(msg Msg) error {
@@ -115,10 +158,10 @@ func (b *InProcBus) AddHandler(handler HandlerFunc) {
b.handlers[queryTypeName] = handler
}
func (b *InProcBus) AddCtxHandler(handler HandlerFunc) {
func (b *InProcBus) AddHandlerCtx(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(1).Elem().Name()
b.handlers[queryTypeName] = handler
b.handlersWithCtx[queryTypeName] = handler
}
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
@@ -137,8 +180,8 @@ func AddHandler(implName string, handler HandlerFunc) {
}
// Package level functions
func AddCtxHandler(implName string, handler HandlerFunc) {
globalBus.AddCtxHandler(handler)
func AddHandlerCtx(implName string, handler HandlerFunc) {
globalBus.AddHandlerCtx(handler)
}
// Package level functions
@@ -162,6 +205,20 @@ func Publish(msg Msg) error {
return globalBus.Publish(msg)
}
// InTransaction starts a transaction and store it in the context.
// The caller can then pass a function with multiple DispatchCtx calls that
// all will be executed in the same transaction. InTransaction will rollback if the
// callback returns an error.
func InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return globalBus.InTransaction(ctx, fn)
}
func ClearBusHandlers() {
globalBus = New()
}
type noopTransactionManager struct{}
func (*noopTransactionManager) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return fn(ctx)
}

View File

@@ -1,24 +1,67 @@
package bus
import (
"context"
"errors"
"fmt"
"testing"
)
type TestQuery struct {
type testQuery struct {
Id int64
Resp string
}
func TestDispatchCtxCanUseNormalHandlers(t *testing.T) {
bus := New()
handlerWithCtxCallCount := 0
handlerCallCount := 0
handlerWithCtx := func(ctx context.Context, query *testQuery) error {
handlerWithCtxCallCount++
return nil
}
handler := func(query *testQuery) error {
handlerCallCount++
return nil
}
err := bus.DispatchCtx(context.Background(), &testQuery{})
if err != ErrHandlerNotFound {
t.Errorf("expected bus to return HandlerNotFound is no handler is registered")
}
bus.AddHandler(handler)
t.Run("when a normal handler is registered", func(t *testing.T) {
bus.Dispatch(&testQuery{})
if handlerCallCount != 1 {
t.Errorf("Expected normal handler to be called 1 time. was called %d", handlerCallCount)
}
t.Run("when a ctx handler is registered", func(t *testing.T) {
bus.AddHandlerCtx(handlerWithCtx)
bus.Dispatch(&testQuery{})
if handlerWithCtxCallCount != 1 {
t.Errorf("Expected ctx handler to be called 1 time. was called %d", handlerWithCtxCallCount)
}
})
})
}
func TestQueryHandlerReturnsError(t *testing.T) {
bus := New()
bus.AddHandler(func(query *TestQuery) error {
bus.AddHandler(func(query *testQuery) error {
return errors.New("handler error")
})
err := bus.Dispatch(&TestQuery{})
err := bus.Dispatch(&testQuery{})
if err == nil {
t.Fatal("Send query failed " + err.Error())
@@ -30,12 +73,12 @@ func TestQueryHandlerReturnsError(t *testing.T) {
func TestQueryHandlerReturn(t *testing.T) {
bus := New()
bus.AddHandler(func(q *TestQuery) error {
bus.AddHandler(func(q *testQuery) error {
q.Resp = "hello from handler"
return nil
})
query := &TestQuery{}
query := &testQuery{}
err := bus.Dispatch(query)
if err != nil {
@@ -49,17 +92,17 @@ func TestEventListeners(t *testing.T) {
bus := New()
count := 0
bus.AddEventListener(func(query *TestQuery) error {
bus.AddEventListener(func(query *testQuery) error {
count += 1
return nil
})
bus.AddEventListener(func(query *TestQuery) error {
bus.AddEventListener(func(query *testQuery) error {
count += 10
return nil
})
err := bus.Publish(&TestQuery{})
err := bus.Publish(&testQuery{})
if err != nil {
t.Fatal("Publish event failed " + err.Error())

View File

@@ -6,6 +6,7 @@ import (
"github.com/codegangsta/cli"
"github.com/fatih/color"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
@@ -15,13 +16,17 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
return func(context *cli.Context) {
cmd := &contextCommandLine{context}
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
Config: cmd.String("config"),
HomePath: cmd.String("homepath"),
Args: flag.Args(),
})
sqlstore.NewEngine()
engine := &sqlstore.SqlStore{}
engine.Cfg = cfg
engine.Bus = bus.GetBus()
engine.Init()
if err := command(cmd); err != nil {
logger.Errorf("\n%s: ", color.RedString("Error"))

View File

@@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
fileInfo, err := os.Stat(pluginsDir)
if err != nil {
if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
return errors.New(fmt.Sprintf("pluginsDir (%s) is not a directory", pluginsDir))
return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
}
return nil
}
@@ -112,7 +112,7 @@ func SelectVersion(plugin m.Plugin, version string) (m.Version, error) {
}
}
return m.Version{}, errors.New("Could not find the version your looking for")
return m.Version{}, errors.New("Could not find the version you're looking for")
}
func RemoveGitBuildFromName(pluginName, filename string) string {
@@ -152,7 +152,7 @@ func downloadFile(pluginName, filePath, url string) (err error) {
return err
}
r, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength)
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return err
}

View File

@@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
return fmt.Errorf("error: %s", err)
}
if pluginDirInfo.IsDir() == false {
if !pluginDirInfo.IsDir() {
return errors.New("plugin path is not a directory")
}

View File

@@ -3,12 +3,11 @@ package commands
import (
"errors"
"fmt"
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"strings"
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
)
var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
func removeCommand(c CommandLine) error {

View File

@@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
for _, p := range pluginsToUpgrade {
logger.Infof("Updating %v \n", p.Id)
var err error
err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
err := s.RemoveInstalledPlugin(pluginsDir, p.Id)
if err != nil {
return err
}

View File

@@ -16,7 +16,7 @@ func upgradeCommand(c CommandLine) error {
return err
}
v, err2 := s.GetPlugin(localPlugin.Id, c.RepoDirectory())
v, err2 := s.GetPlugin(pluginName, c.RepoDirectory())
if err2 != nil {
return err2
@@ -24,9 +24,9 @@ func upgradeCommand(c CommandLine) error {
if ShouldUpgrade(localPlugin.Info.Version, v) {
s.RemoveInstalledPlugin(pluginsDir, pluginName)
return InstallPlugin(localPlugin.Id, "", c)
return InstallPlugin(pluginName, "", c)
}
logger.Infof("%s %s is up to date \n", color.GreenString("✔"), localPlugin.Id)
logger.Infof("%s %s is up to date \n", color.GreenString("✔"), pluginName)
return nil
}

View File

@@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"path"
"runtime"
"time"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
@@ -42,7 +43,7 @@ func Init(version string, skipTLSVerify bool) {
}
HttpClient = http.Client{
Timeout: time.Duration(10 * time.Second),
Timeout: 10 * time.Second,
Transport: tr,
}
}
@@ -62,7 +63,7 @@ func ListAllPlugins(repoUrl string) (m.PluginRepo, error) {
var data m.PluginRepo
err = json.Unmarshal(body, &data)
if err != nil {
logger.Info("Failed to unmarshal graphite response error: %v", err)
logger.Info("Failed to unmarshal plugin repo response error:", err)
return m.PluginRepo{}, err
}
@@ -139,7 +140,7 @@ func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
var data m.Plugin
err = json.Unmarshal(body, &data)
if err != nil {
logger.Info("Failed to unmarshal graphite response error: %v", err)
logger.Info("Failed to unmarshal plugin repo response error:", err)
return m.Plugin{}, err
}
@@ -155,6 +156,8 @@ func sendRequest(repoUrl string, subPaths ...string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
req.Header.Set("grafana-version", grafanaVersion)
req.Header.Set("grafana-os", runtime.GOOS)
req.Header.Set("grafana-arch", runtime.GOARCH)
req.Header.Set("User-Agent", "grafana "+grafanaVersion)
if err != nil {

View File

@@ -42,6 +42,8 @@ func returnOsDefault(currentOs string) string {
return "/usr/local/var/lib/grafana/plugins"
case "freebsd":
return "/var/db/grafana/plugins"
case "openbsd":
return "/var/grafana/plugins"
default: //"linux"
return "/var/lib/grafana/plugins"
}

View File

@@ -3,6 +3,8 @@ package main
import (
"flag"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"runtime"
@@ -11,34 +13,32 @@ import (
"syscall"
"time"
"net/http"
_ "net/http/pprof"
extensions "github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/log"
"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/setting"
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
_ "github.com/grafana/grafana/pkg/tsdb/elasticsearch"
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
_ "github.com/grafana/grafana/pkg/tsdb/influxdb"
_ "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/stackdriver"
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
)
var version = "5.0.0"
var commit = "NA"
var buildBranch = "master"
var buildstamp string
var build_date string
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
var pidFile = flag.String("pidfile", "", "path to pid file")
var exitChan = make(chan int)
func main() {
v := flag.Bool("v", false, "prints current version and exits")
@@ -46,7 +46,7 @@ func main() {
profilePort := flag.Int("profile-port", 6060, "Define custom port for profiling")
flag.Parse()
if *v {
fmt.Printf("Version %s (commit: %s)\n", version, commit)
fmt.Printf("Version %s (commit: %s, branch: %s)\n", version, commit, buildBranch)
os.Exit(0)
}
@@ -77,45 +77,37 @@ func main() {
setting.BuildVersion = version
setting.BuildCommit = commit
setting.BuildStamp = buildstampInt64
setting.BuildBranch = buildBranch
setting.IsEnterprise = extensions.IsEnterprise
metrics.SetBuildInformation(version, commit, buildBranch)
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
shutdownCompleted := make(chan int)
server := NewGrafanaServer()
go listenToSystemSignals(server, shutdownCompleted)
go listenToSystemSignals(server)
go func() {
code := 0
if err := server.Start(); err != nil {
log.Error2("Startup failed", "error", err)
code = 1
}
err := server.Run()
exitChan <- code
}()
code := <-shutdownCompleted
log.Info2("Grafana shutdown completed.", "code", code)
code := server.Exit(err)
trace.Stop()
log.Close()
os.Exit(code)
}
func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
func listenToSystemSignals(server *GrafanaServerImpl) {
signalChan := make(chan os.Signal, 1)
ignoreChan := make(chan os.Signal, 1)
code := 0
sighupChan := make(chan os.Signal, 1)
signal.Notify(ignoreChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
signal.Notify(sighupChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
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
for {
select {
case <-sighupChan:
log.Reload()
case sig := <-signalChan:
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
}
}
}

View File

@@ -11,24 +11,33 @@ import (
"strconv"
"time"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/facebookgo/inject"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/social"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
"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/services/cache"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
"github.com/grafana/grafana/pkg/tracing"
// self registering services
_ "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/metrics"
_ "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/notifications"
_ "github.com/grafana/grafana/pkg/services/provisioning"
_ "github.com/grafana/grafana/pkg/services/rendering"
_ "github.com/grafana/grafana/pkg/services/search"
_ "github.com/grafana/grafana/pkg/services/sqlstore"
_ "github.com/grafana/grafana/pkg/tracing"
)
func NewGrafanaServer() *GrafanaServerImpl {
@@ -40,110 +49,143 @@ func NewGrafanaServer() *GrafanaServerImpl {
shutdownFn: shutdownFn,
childRoutines: childRoutines,
log: log.New("server"),
cfg: setting.NewCfg(),
}
}
type GrafanaServerImpl struct {
context context.Context
shutdownFn context.CancelFunc
childRoutines *errgroup.Group
log log.Logger
context context.Context
shutdownFn context.CancelFunc
childRoutines *errgroup.Group
log log.Logger
cfg *setting.Cfg
shutdownReason string
shutdownInProgress bool
httpServer *api.HttpServer
RouteRegister routing.RouteRegister `inject:""`
HttpServer *api.HTTPServer `inject:""`
}
func (g *GrafanaServerImpl) Start() error {
g.initLogging()
func (g *GrafanaServerImpl) Run() error {
g.loadConfiguration()
g.writePIDFile()
initSql()
metrics.Init(setting.Cfg)
search.Init()
login.Init()
social.NewOAuthService()
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) })
serviceGraph := inject.Graph{}
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
serviceGraph.Provide(&inject.Object{Value: g.cfg})
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil {
return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
// self registered services
services := registry.GetServices()
// Add all services to dependency graph
for _, service := range services {
serviceGraph.Provide(&inject.Object{Value: service.Instance})
}
tracingCloser, err := tracing.Init(setting.Cfg)
if err != nil {
return fmt.Errorf("Tracing settings is not valid. error: %v", err)
}
defer tracingCloser.Close()
serviceGraph.Provide(&inject.Object{Value: g})
// init alerting
if setting.AlertingEnabled && setting.ExecuteAlerts {
engine := alerting.NewEngine()
g.childRoutines.Go(func() error { return engine.Run(g.context) })
// Inject dependencies to services
if err := serviceGraph.Populate(); err != nil {
return fmt.Errorf("Failed to populate service dependency: %v", err)
}
// cleanup service
cleanUpService := cleanup.NewCleanUpService()
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
// Init & start services
for _, service := range services {
if registry.IsDisabled(service.Instance) {
continue
}
if err = notifications.Init(); err != nil {
return fmt.Errorf("Notification service failed to initialize. error: %v", err)
g.log.Info("Initializing " + service.Name)
if err := service.Instance.Init(); err != nil {
return fmt.Errorf("Service init failed: %v", err)
}
}
// Start background services
for _, srv := range services {
// variable needed for accessing loop variable in function callback
descriptor := srv
service, ok := srv.Instance.(registry.BackgroundService)
if !ok {
continue
}
if registry.IsDisabled(descriptor.Instance) {
continue
}
g.childRoutines.Go(func() error {
// Skip starting new service when shutting down
// Can happen when service stop/return during startup
if g.shutdownInProgress {
return nil
}
err := service.Run(g.context)
// If error is not canceled then the service crashed
if err != context.Canceled && err != nil {
g.log.Error("Stopped "+descriptor.Name, "reason", err)
} else {
g.log.Info("Stopped "+descriptor.Name, "reason", err)
}
// Mark that we are in shutdown mode
// So more services are not started
g.shutdownInProgress = true
return err
})
}
sendSystemdNotification("READY=1")
return g.startHttpServer()
return g.childRoutines.Wait()
}
func initSql() {
sqlstore.NewEngine()
sqlstore.EnsureAdminUser()
}
func (g *GrafanaServerImpl) initLogging() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
func (g *GrafanaServerImpl) loadConfiguration() {
err := g.cfg.Load(&setting.CommandLineArgs{
Config: *configFile,
HomePath: *homePath,
Args: flag.Args(),
})
if err != nil {
g.log.Error(err.Error())
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()
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "branch", buildBranch, "compiled", time.Unix(setting.BuildStamp, 0))
g.cfg.LogConfigSources()
}
func (g *GrafanaServerImpl) startHttpServer() error {
g.httpServer = api.NewHttpServer()
err := g.httpServer.Start(g.context)
if err != nil {
return fmt.Errorf("Fail to start server. error: %v", err)
}
return nil
}
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
g.log.Info("Shutdown started", "code", code, "reason", reason)
err := g.httpServer.Shutdown(g.context)
if err != nil {
g.log.Error("Failed to shutdown server", "error", err)
}
func (g *GrafanaServerImpl) Shutdown(reason string) {
g.log.Info("Shutdown started", "reason", reason)
g.shutdownReason = reason
g.shutdownInProgress = true
// call cancel func on root context
g.shutdownFn()
err = g.childRoutines.Wait()
if err != nil && err != context.Canceled {
g.log.Error("Server shutdown completed with an error", "error", err)
// wait for child routines
g.childRoutines.Wait()
}
func (g *GrafanaServerImpl) Exit(reason error) int {
// default exit code is 1
code := 1
if reason == context.Canceled && g.shutdownReason != "" {
reason = fmt.Errorf(g.shutdownReason)
code = 0
}
g.log.Error("Server shutdown", "reason", reason)
return code
}
func (g *GrafanaServerImpl) writePIDFile() {

View File

@@ -33,7 +33,7 @@ func New(orgId int64, name string) KeyGenResult {
jsonString, _ := json.Marshal(jsonKey)
result.ClientSecret = base64.StdEncoding.EncodeToString([]byte(jsonString))
result.ClientSecret = base64.StdEncoding.EncodeToString(jsonString)
return result
}
@@ -44,7 +44,7 @@ func Decode(keyString string) (*ApiKeyJson, error) {
}
var keyObj ApiKeyJson
err = json.Unmarshal([]byte(jsonString), &keyObj)
err = json.Unmarshal(jsonString, &keyObj)
if err != nil {
return nil, ErrInvalidApiKey
}

View File

@@ -6,7 +6,6 @@ import (
"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"
@@ -15,11 +14,8 @@ import (
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
@@ -145,5 +141,9 @@ func getDiff(baseData, newData *simplejson.Json) (interface{}, diff.Diff, error)
left := make(map[string]interface{})
err = json.Unmarshal(leftBytes, &left)
if err != nil {
return nil, nil, err
}
return left, jsonDiff, nil
}

View File

@@ -22,7 +22,7 @@ const (
)
var (
// changeTypeToSymbol is used for populating the terminating characer in
// changeTypeToSymbol is used for populating the terminating character in
// the diff
changeTypeToSymbol = map[ChangeType]string{
ChangeNil: "",

View File

@@ -134,9 +134,8 @@ func (v *Value) get(key string) (*Value, error) {
child, ok := obj.Map()[key]
if ok {
return child, nil
} else {
return nil, KeyNotFoundError{key}
}
return nil, KeyNotFoundError{key}
}
return nil, err
@@ -174,17 +173,13 @@ func (v *Object) GetObject(keys ...string) (*Object, error) {
if err != nil {
return nil, err
} else {
obj, err := child.Object()
if err != nil {
return nil, err
} else {
return obj, nil
}
}
obj, err := child.Object()
if err != nil {
return nil, err
}
return obj, nil
}
// Gets the value at key path and attempts to typecast the value into a string.
@@ -196,18 +191,17 @@ func (v *Object) GetString(keys ...string) (string, error) {
if err != nil {
return "", err
} else {
return child.String()
}
return child.String()
}
func (v *Object) MustGetString(path string, def string) string {
keys := strings.Split(path, ".")
if str, err := v.GetString(keys...); err != nil {
str, err := v.GetString(keys...)
if err != nil {
return def
} else {
return str
}
return str
}
// Gets the value at key path and attempts to typecast the value into null.
@@ -233,16 +227,13 @@ func (v *Object) GetNumber(keys ...string) (json.Number, error) {
if err != nil {
return "", err
} else {
n, err := child.Number()
if err != nil {
return "", err
} else {
return n, nil
}
}
n, err := child.Number()
if err != nil {
return "", err
}
return n, nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@@ -254,16 +245,13 @@ func (v *Object) GetFloat64(keys ...string) (float64, error) {
if err != nil {
return 0, err
} else {
n, err := child.Float64()
if err != nil {
return 0, err
} else {
return n, nil
}
}
n, err := child.Float64()
if err != nil {
return 0, err
}
return n, nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@@ -275,16 +263,13 @@ func (v *Object) GetInt64(keys ...string) (int64, error) {
if err != nil {
return 0, err
} else {
n, err := child.Int64()
if err != nil {
return 0, err
} else {
return n, nil
}
}
n, err := child.Int64()
if err != nil {
return 0, err
}
return n, nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@@ -296,9 +281,8 @@ func (v *Object) GetInterface(keys ...string) (interface{}, error) {
if err != nil {
return nil, err
} else {
return child.Interface(), nil
}
return child.Interface(), nil
}
// Gets the value at key path and attempts to typecast the value into a bool.
@@ -311,7 +295,6 @@ func (v *Object) GetBoolean(keys ...string) (bool, error) {
if err != nil {
return false, err
}
return child.Boolean()
}
@@ -328,11 +311,8 @@ func (v *Object) GetValueArray(keys ...string) ([]*Value, error) {
if err != nil {
return nil, err
} else {
return child.Array()
}
return child.Array()
}
// Gets the value at key path and attempts to typecast the value into an array of objects.
@@ -347,30 +327,24 @@ func (v *Object) GetObjectArray(keys ...string) ([]*Object, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]*Object, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.
Object()
if err != nil {
return nil, err
} else {
typedArray := make([]*Object, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.
Object()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of string.
@@ -387,29 +361,23 @@ func (v *Object) GetStringArray(keys ...string) ([]string, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]string, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.String()
if err != nil {
return nil, err
} else {
typedArray := make([]string, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.String()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of numbers.
@@ -424,29 +392,23 @@ func (v *Object) GetNumberArray(keys ...string) ([]json.Number, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]json.Number, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Number()
if err != nil {
return nil, err
} else {
typedArray := make([]json.Number, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Number()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of floats.
@@ -456,29 +418,23 @@ func (v *Object) GetFloat64Array(keys ...string) ([]float64, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]float64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Float64()
if err != nil {
return nil, err
} else {
typedArray := make([]float64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Float64()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of ints.
@@ -488,29 +444,23 @@ func (v *Object) GetInt64Array(keys ...string) ([]int64, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]int64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Int64()
if err != nil {
return nil, err
} else {
typedArray := make([]int64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Int64()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of bools.
@@ -520,29 +470,23 @@ func (v *Object) GetBooleanArray(keys ...string) ([]bool, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]bool, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Boolean()
if err != nil {
return nil, err
} else {
typedArray := make([]bool, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Boolean()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of nulls.
@@ -552,29 +496,23 @@ func (v *Object) GetNullArray(keys ...string) (int64, error) {
if err != nil {
return 0, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return 0, err
}
var length int64 = 0
for _, arrayItem := range array {
err := arrayItem.Null()
if err != nil {
return 0, err
} else {
var length int64 = 0
for _, arrayItem := range array {
err := arrayItem.Null()
if err != nil {
return 0, err
} else {
length++
}
}
return length, nil
}
length++
}
return length, nil
}
// Returns an error if the value is not actually null
@@ -585,15 +523,12 @@ func (v *Value) Null() error {
switch v.data.(type) {
case nil:
valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
break
}
if valid {
return nil
}
return ErrNotNull
}
// Attempts to typecast the current value into an array.
@@ -607,24 +542,19 @@ func (v *Value) Array() ([]*Value, error) {
switch v.data.(type) {
case []interface{}:
valid = true
break
}
// Unsure if this is a good way to use slices, it's probably not
var slice []*Value
if valid {
for _, element := range v.data.([]interface{}) {
child := Value{element, true}
slice = append(slice, &child)
}
return slice, nil
}
return slice, ErrNotArray
}
// Attempts to typecast the current value into a number.
@@ -638,7 +568,6 @@ func (v *Value) Number() (json.Number, error) {
switch v.data.(type) {
case json.Number:
valid = true
break
}
if valid {
@@ -687,7 +616,6 @@ func (v *Value) Boolean() (bool, error) {
switch v.data.(type) {
case bool:
valid = true
break
}
if valid {
@@ -709,7 +637,6 @@ func (v *Value) Object() (*Object, error) {
switch v.data.(type) {
case map[string]interface{}:
valid = true
break
}
if valid {
@@ -746,7 +673,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
switch v.data.(type) {
case []interface{}:
valid = true
break
}
// Unsure if this is a good way to use slices, it's probably not
@@ -782,7 +708,6 @@ func (v *Value) String() (string, error) {
switch v.data.(type) {
case string:
valid = true
break
}
if valid {

View File

@@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
}
func (assert *Assert) True(value bool, message string) {
if value == false {
if !value {
log.Panicln("Assert: ", message)
}
}
@@ -60,6 +60,7 @@ func TestFirst(t *testing.T) {
}`
j, err := NewObjectFromBytes([]byte(testJSON))
assert.True(err == nil, "failed to create new object from bytes")
a, err := j.GetObject("address")
assert.True(a != nil && err == nil, "failed to create json from string")
@@ -76,10 +77,10 @@ func TestFirst(t *testing.T) {
assert.True(s == "fallback", "must get string return fallback")
s, err = j.GetString("name")
assert.True(s == "anton" && err == nil, "name shoud match")
assert.True(s == "anton" && err == nil, "name should match")
s, err = j.GetString("address", "street")
assert.True(s == "Street 42" && err == nil, "street shoud match")
assert.True(s == "Street 42" && err == nil, "street should match")
//log.Println("s: ", s.String())
_, err = j.GetNumber("age")
@@ -108,6 +109,7 @@ func TestFirst(t *testing.T) {
//log.Println("address: ", address)
s, err = address.GetString("street")
assert.True(s == "Street 42" && err == nil, "street mismatching")
addressAsString, err := j.GetString("address")
assert.True(addressAsString == "" && err != nil, "address should not be an string")
@@ -119,13 +121,13 @@ func TestFirst(t *testing.T) {
assert.True(s == "" && err != nil, "nonexistent string fail")
b, err := j.GetBoolean("true")
assert.True(b == true && err == nil, "bool true test")
assert.True(b && err == nil, "bool true test")
b, err = j.GetBoolean("false")
assert.True(b == false && err == nil, "bool false test")
assert.True(!b && err == nil, "bool false test")
b, err = j.GetBoolean("invalid_field")
assert.True(b == false && err != nil, "bool invalid test")
assert.True(!b && err != nil, "bool invalid test")
list, err := j.GetValueArray("list")
assert.True(list != nil && err == nil, "list should be an array")
@@ -148,6 +150,7 @@ func TestFirst(t *testing.T) {
//assert.True(element.IsObject() == true, "first fail")
element, err := elementValue.Object()
assert.True(err == nil, "create element fail")
s, err = element.GetString("street")
assert.True(s == "Street 42" && err == nil, "second fail")
@@ -232,6 +235,7 @@ func TestSecond(t *testing.T) {
assert.True(fromName == "Tom Brady" && err == nil, "fromName mismatch")
actions, err := dataItem.GetObjectArray("actions")
assert.True(err == nil, "get object from array failed")
for index, action := range actions {

View File

@@ -52,7 +52,7 @@ func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (
}
randomFileName := util.GetRandomString(30) + ".png"
// upload image
az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName)
az.log.Debug("Uploading image to azure_blob", "container_name", az.container_name, "blob_name", randomFileName)
resp, err := blob.FileUpload(az.container_name, randomFileName, file)
if err != nil {
return "", err
@@ -127,8 +127,6 @@ type xmlError struct {
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
@@ -225,7 +223,7 @@ func (a *Auth) SignRequest(req *http.Request) {
)
decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
sha256 := hmac.New(sha256.New, []byte(decodedKey))
sha256 := hmac.New(sha256.New, decodedKey)
sha256.Write([]byte(strToSign))
signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
@@ -274,10 +272,10 @@ func (a *Auth) canonicalizedHeaders(req *http.Request) string {
}
}
splitted := strings.Split(buffer.String(), "\n")
sort.Strings(splitted)
split := strings.Split(buffer.String(), "\n")
sort.Strings(split)
return strings.Join(splitted, "\n")
return strings.Join(split, "\n")
}
/*
@@ -313,8 +311,8 @@ func (a *Auth) canonicalizedResource(req *http.Request) string {
buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ",")))
}
splitted := strings.Split(buffer.String(), "\n")
sort.Strings(splitted)
split := strings.Split(buffer.String(), "\n")
sort.Strings(split)
return strings.Join(splitted, "\n")
return strings.Join(split, "\n")
}

View File

@@ -10,9 +10,11 @@ import (
func TestUploadToAzureBlob(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.azure_blob", t, func() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
err := cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
So(err, ShouldBeNil)
uploader, _ := NewImageUploader()

View File

@@ -10,7 +10,8 @@ import (
func TestUploadToGCS(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.gcs", t, func() {
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})

View File

@@ -3,9 +3,10 @@ package imguploader
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/log"
"regexp"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
)
@@ -24,7 +25,7 @@ func NewImageUploader() (ImageUploader, error) {
switch setting.ImageUploadProvider {
case "s3":
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
if err != nil {
return nil, err
}
@@ -51,7 +52,7 @@ func NewImageUploader() (ImageUploader, error) {
return NewS3Uploader(region, bucket, path, "public-read", accessKey, secretKey), nil
case "webdav":
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
webdavSec, err := setting.Raw.GetSection("external_image_storage.webdav")
if err != nil {
return nil, err
}
@@ -67,7 +68,7 @@ func NewImageUploader() (ImageUploader, error) {
return NewWebdavImageUploader(url, username, password, public_url)
case "gcs":
gcssec, err := setting.Cfg.GetSection("external_image_storage.gcs")
gcssec, err := setting.Raw.GetSection("external_image_storage.gcs")
if err != nil {
return nil, err
}
@@ -78,7 +79,7 @@ func NewImageUploader() (ImageUploader, error) {
return NewGCSUploader(keyFile, bucketName, path), nil
case "azure_blob":
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
azureBlobSec, err := setting.Raw.GetSection("external_image_storage.azure_blob")
if err != nil {
return nil, err
}

View File

@@ -11,14 +11,16 @@ import (
func TestImageUploaderFactory(t *testing.T) {
Convey("Can create image uploader for ", t, func() {
Convey("S3ImageUploader config", func() {
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "s3"
Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
@@ -36,7 +38,8 @@ func TestImageUploaderFactory(t *testing.T) {
})
Convey("with bucket url https://s3.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
@@ -54,16 +57,16 @@ func TestImageUploaderFactory(t *testing.T) {
})
Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*S3Uploader)
original, ok := uploader.(*S3Uploader)
So(ok, ShouldBeTrue)
So(original.region, ShouldEqual, "us-west-2")
So(original.bucket, ShouldEqual, "my.bucket.com")
@@ -75,13 +78,15 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("Webdav uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "webdav"
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
webdavSec, err := cfg.Raw.GetSection("external_image_storage.webdav")
So(err, ShouldBeNil)
webdavSec.NewKey("url", "webdavUrl")
webdavSec.NewKey("username", "username")
webdavSec.NewKey("password", "password")
@@ -100,43 +105,45 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("GCS uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "gcs"
gcpSec, err := setting.Cfg.GetSection("external_image_storage.gcs")
gcpSec, err := cfg.Raw.GetSection("external_image_storage.gcs")
So(err, ShouldBeNil)
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)
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{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "azure_blob"
Convey("with container name", func() {
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
azureBlobSec, err := cfg.Raw.GetSection("external_image_storage.azure_blob")
So(err, ShouldBeNil)
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)
original, ok := uploader.(*AzureBlobUploader)
So(ok, ShouldBeTrue)
So(original.account_name, ShouldEqual, "account_name")
So(original.account_key, ShouldEqual, "account_key")
@@ -147,7 +154,8 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("Local uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})

View File

@@ -2,12 +2,15 @@ package imguploader
import (
"context"
"fmt"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"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/defaults"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
@@ -50,7 +53,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
SecretAccessKey: u.secretKey,
}},
&credentials.EnvProvider{},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
remoteCredProvider(sess),
})
cfg := &aws.Config{
Region: aws.String(u.region),
@@ -60,7 +63,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
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)
log.Debug("Uploading image to s3. url = %s", image_url)
file, err := os.Open(imageDiskPath)
if err != nil {
@@ -85,3 +88,27 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
}
return image_url, 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`
d := defaults.Get()
return endpointcreds.NewProviderClient(
*d.Config,
d.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}
}

View File

@@ -10,7 +10,8 @@ import (
func TestUploadToS3(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.s3", t, func() {
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})

View File

@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/grafana/grafana/pkg/util"
@@ -35,20 +36,36 @@ var netClient = &http.Client{
Transport: netTransport,
}
func (u *WebdavUploader) PublicURL(filename string) string {
if strings.Contains(u.public_url, "${file}") {
return strings.Replace(u.public_url, "${file}", filename, -1)
} else {
publicURL, _ := url.Parse(u.public_url)
publicURL.Path = path.Join(publicURL.Path, filename)
return publicURL.String()
}
}
func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
url, _ := url.Parse(u.url)
filename := util.GetRandomString(20) + ".png"
url.Path = path.Join(url.Path, filename)
imgData, err := ioutil.ReadFile(pa)
if err != nil {
return "", err
}
req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData))
if err != nil {
return "", err
}
if u.username != "" {
req.SetBasicAuth(u.username, u.password)
}
res, err := netClient.Do(req)
if err != nil {
return "", err
}
@@ -59,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
}
if u.public_url != "" {
publicURL, _ := url.Parse(u.public_url)
publicURL.Path = path.Join(publicURL.Path, filename)
return publicURL.String(), nil
return u.PublicURL(filename), nil
}
return url.String(), nil

View File

@@ -2,6 +2,7 @@ package imguploader
import (
"context"
"net/url"
"testing"
. "github.com/smartystreets/goconvey/convey"
@@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) {
So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
})
}
func TestPublicURL(t *testing.T) {
Convey("Given a public URL with parameters, and no template", t, func() {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=")
parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png"))
So(parsed.Path, ShouldEndWith, "fileyfile.png")
})
Convey("Given a public URL with parameters, and a template", t, func() {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}")
So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png")
})
}

View File

@@ -8,6 +8,10 @@ import (
"strconv"
)
const (
nullString = "null"
)
// Float is a nullable float64.
// It does not consider zero values to be null.
// It will decode to null, not zero, if null.
@@ -50,7 +54,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
}
switch x := v.(type) {
case float64:
f.Float64 = float64(x)
f.Float64 = x
case map[string]interface{}:
err = json.Unmarshal(data, &f.NullFloat64)
case nil:
@@ -68,7 +72,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
// It will return an error if the input is not an integer, blank, or "null".
func (f *Float) UnmarshalText(text []byte) error {
str := string(text)
if str == "" || str == "null" {
if str == "" || str == nullString {
f.Valid = false
return nil
}
@@ -82,7 +86,7 @@ func (f *Float) UnmarshalText(text []byte) error {
// It will encode null if this Float is null.
func (f Float) MarshalJSON() ([]byte, error) {
if !f.Valid {
return []byte("null"), nil
return []byte(nullString), nil
}
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
}
@@ -100,12 +104,21 @@ func (f Float) MarshalText() ([]byte, error) {
// It will encode a blank string if this Float is null.
func (f Float) String() string {
if !f.Valid {
return "null"
return nullString
}
return fmt.Sprintf("%1.3f", f.Float64)
}
// FullString returns float as string in full precision
func (f Float) FullString() string {
if !f.Valid {
return nullString
}
return fmt.Sprintf("%f", f.Float64)
}
// SetValid changes this Float's value and also sets it to be non-null.
func (f *Float) SetValid(n float64) {
f.Float64 = n

View File

@@ -1,161 +0,0 @@
package renderer
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
"strconv"
"strings"
"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
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 {
// invert offset
if strings.HasPrefix(isoOffset, "UTC+") {
return strings.Replace(isoOffset, "UTC+", "UTC-", 1)
}
if strings.HasPrefix(isoOffset, "UTC-") {
return strings.Replace(isoOffset, "UTC-", "UTC+", 1)
}
return isoOffset
}
func appendEnviron(baseEnviron []string, name string, value string) []string {
results := make([]string, 0)
prefix := fmt.Sprintf("%s=", name)
for _, v := range baseEnviron {
if !strings.HasPrefix(v, prefix) {
results = append(results, v)
}
}
return append(results, fmt.Sprintf("%s=%s", name, value))
}
func RenderToPng(params *RenderOpts) (string, error) {
rendererLog.Info("Rendering", "path", params.Path)
var executable = "phantomjs"
if runtime.GOOS == "windows" {
executable = executable + ".exe"
}
localDomain := "localhost"
if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
localDomain = setting.HttpAddr
}
// &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"
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...)
output, err := cmd.StdoutPipe()
if err != nil {
rendererLog.Error("Could not acquire stdout pipe", err)
return "", err
}
cmd.Stderr = cmd.Stdout
if params.Timezone != "" {
baseEnviron := os.Environ()
cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(params.Timezone))
}
err = cmd.Start()
if err != nil {
rendererLog.Error("Could not start command", err)
return "", err
}
logWriter := log.NewLogWriter(rendererLog, log.LvlDebug, "[phantom] ")
go io.Copy(logWriter, output)
done := make(chan error)
go func() {
if err := cmd.Wait(); err != nil {
rendererLog.Error("failed to render an image", "error", err)
}
close(done)
}()
select {
case <-time.After(time.Duration(timeout) * time.Second):
if err := cmd.Process.Kill(); err != nil {
rendererLog.Error("failed to kill", "error", err)
}
return "", ErrTimeout
case <-done:
}
rendererLog.Debug("Image rendered", "path", pngPath)
return pngPath, nil
}

View File

@@ -1,35 +0,0 @@
package renderer
//
// import (
// "io/ioutil"
// "os"
// "testing"
//
// . "github.com/smartystreets/goconvey/convey"
// )
//
// func TestPhantomRender(t *testing.T) {
//
// Convey("Can render url", t, func() {
// tempDir, _ := ioutil.TempDir("", "img")
// ipng, err := RenderToPng("http://www.google.com")
// So(err, ShouldBeNil)
// So(exists(png), ShouldEqual, true)
//
// //_, err = os.Stat(store.getFilePathForDashboard("hello"))
// //So(err, ShouldBeNil)
// })
//
// }
//
// func exists(path string) bool {
// _, err := os.Stat(path)
// if err == nil {
// return true
// }
// if os.IsNotExist(err) {
// return false
// }
// return false
// }

View File

@@ -256,7 +256,7 @@ func (j *Json) StringArray() ([]string, error) {
// MustArray guarantees the return of a `[]interface{}` (with optional default)
//
// useful when you want to interate over array values in a succinct manner:
// useful when you want to iterate over array values in a succinct manner:
// for i, v := range js.Get("results").MustArray() {
// fmt.Println(i, v)
// }
@@ -281,7 +281,7 @@ func (j *Json) MustArray(args ...[]interface{}) []interface{} {
// MustMap guarantees the return of a `map[string]interface{}` (with optional default)
//
// useful when you want to interate over map values in a succinct manner:
// useful when you want to iterate over map values in a succinct manner:
// for k, v := range js.Get("dictionary").MustMap() {
// fmt.Println(k, v)
// }
@@ -329,7 +329,7 @@ func (j *Json) MustString(args ...string) string {
// MustStringArray guarantees the return of a `[]string` (with optional default)
//
// useful when you want to interate over array values in a succinct manner:
// useful when you want to iterate over array values in a succinct manner:
// for i, s := range js.Get("results").MustStringArray() {
// fmt.Println(i, s)
// }

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