Merge branch 'master' into master

This commit is contained in:
Torkel Ödegaard
2018-05-08 21:23:34 +02:00
committed by GitHub
547 changed files with 19074 additions and 7634 deletions
+6 -13
View File
@@ -13,11 +13,7 @@ func init() {
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
if _, err := extractor.GetAlerts(); err != nil {
return err
}
return nil
return extractor.ValidateAlerts()
}
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
@@ -29,15 +25,12 @@ func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
if alerts, err := extractor.GetAlerts(); err != nil {
return err
} else {
saveAlerts.Alerts = alerts
}
if err := bus.Dispatch(&saveAlerts); err != nil {
alerts, err := extractor.GetAlerts()
if err != nil {
return err
}
return nil
saveAlerts.Alerts = alerts
return bus.Dispatch(&saveAlerts)
}
@@ -9,8 +9,8 @@ import (
)
var (
defaultTypes []string = []string{"gt", "lt"}
rangedTypes []string = []string{"within_range", "outside_range"}
defaultTypes = []string{"gt", "lt"}
rangedTypes = []string{"within_range", "outside_range"}
)
type AlertEvaluator interface {
@@ -20,7 +20,7 @@ type AlertEvaluator interface {
type NoValueEvaluator struct{}
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
return reducedValue.Valid == false
return !reducedValue.Valid
}
type ThresholdEvaluator struct {
@@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
}
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
if reducedValue.Valid == false {
if !reducedValue.Valid {
return false
}
@@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
}
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
if reducedValue.Valid == false {
if !reducedValue.Valid {
return false
}
+1 -1
View File
@@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
if reducedValue.Valid == false {
if !reducedValue.Valid {
emptySerieCount++
}
+34 -26
View File
@@ -11,12 +11,14 @@ import (
"github.com/benbjohnson/clock"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
"golang.org/x/sync/errgroup"
)
type Engine struct {
execQueue chan *Job
clock clock.Clock
type AlertingService struct {
execQueue chan *Job
//clock clock.Clock
ticker *Ticker
scheduler Scheduler
evalHandler EvalHandler
@@ -25,35 +27,41 @@ type Engine struct {
resultHandler ResultHandler
}
func NewEngine() *Engine {
e := &Engine{
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
execQueue: make(chan *Job, 1000),
scheduler: NewScheduler(),
evalHandler: NewEvalHandler(),
ruleReader: NewRuleReader(),
log: log.New("alerting.engine"),
resultHandler: NewResultHandler(),
}
func init() {
registry.RegisterService(&AlertingService{})
}
func NewEngine() *AlertingService {
e := &AlertingService{}
e.Init()
return e
}
func (e *Engine) Run(ctx context.Context) error {
e.log.Info("Initializing Alerting")
func (e *AlertingService) IsDisabled() bool {
return !setting.AlertingEnabled || !setting.ExecuteAlerts
}
func (e *AlertingService) Init() error {
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
e.execQueue = make(chan *Job, 1000)
e.scheduler = NewScheduler()
e.evalHandler = NewEvalHandler()
e.ruleReader = NewRuleReader()
e.log = log.New("alerting.engine")
e.resultHandler = NewResultHandler()
return nil
}
func (e *AlertingService) Run(ctx context.Context) error {
alertGroup, ctx := errgroup.WithContext(ctx)
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
err := alertGroup.Wait()
e.log.Info("Stopped Alerting", "reason", err)
return err
}
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
func (e *AlertingService) alertingTicker(grafanaCtx context.Context) error {
defer func() {
if err := recover(); err != nil {
e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
@@ -78,7 +86,7 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
}
}
func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
for {
@@ -92,13 +100,13 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
}
var (
unfinishedWorkTimeout time.Duration = time.Second * 5
unfinishedWorkTimeout = time.Second * 5
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
alertTimeout time.Duration = time.Second * 30
alertMaxAttempts int = 3
alertTimeout = time.Second * 30
alertMaxAttempts = 3
)
func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
defer func() {
if err := recover(); err != nil {
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
@@ -133,7 +141,7 @@ func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error
}
}
func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
func (e *AlertingService) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
job.Running = false
close(cancelChan)
for cancelFn := range cancelChan {
@@ -142,7 +150,7 @@ func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job)
return err
}
func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
defer func() {
if err := recover(); err != nil {
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
+4 -4
View File
@@ -10,7 +10,7 @@ import (
)
type FakeEvalHandler struct {
SuccessCallID int // 0 means never sucess
SuccessCallID int // 0 means never success
CallNb int
}
@@ -87,7 +87,7 @@ func TestEngineProcessJob(t *testing.T) {
Convey("Should trigger as many retries as needed", func() {
Convey("never sucess -> max retries number", func() {
Convey("never success -> max retries number", func() {
expectedAttempts := alertMaxAttempts
evalHandler := NewFakeEvalHandler(0)
engine.evalHandler = evalHandler
@@ -96,7 +96,7 @@ func TestEngineProcessJob(t *testing.T) {
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
})
Convey("always sucess -> never retry", func() {
Convey("always success -> never retry", func() {
expectedAttempts := 1
evalHandler := NewFakeEvalHandler(1)
engine.evalHandler = evalHandler
@@ -105,7 +105,7 @@ func TestEngineProcessJob(t *testing.T) {
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
})
Convey("some errors before sucess -> some retries", func() {
Convey("some errors before success -> some retries", func() {
expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
evalHandler := NewFakeEvalHandler(expectedAttempts)
engine.evalHandler = evalHandler
+3 -3
View File
@@ -106,11 +106,11 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
return setting.AppUrl, nil
}
if ref, err := c.GetDashboardUID(); err != nil {
ref, err := c.GetDashboardUID()
if err != nil {
return "", err
} else {
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
}
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
}
func (c *EvalContext) GetNewState() m.AlertStateType {
+58 -41
View File
@@ -11,76 +11,78 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
// DashAlertExtractor extracts alerts from the dashboard json
type DashAlertExtractor struct {
Dash *m.Dashboard
OrgId int64
OrgID int64
log log.Logger
}
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor {
// NewDashAlertExtractor returns a new DashAlertExtractor
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
return &DashAlertExtractor{
Dash: dash,
OrgId: orgId,
OrgID: orgID,
log: log.New("alerting.extractor"),
}
}
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, error) {
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*m.DataSource, error) {
if dsName == "" {
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
query := &m.GetDataSourcesQuery{OrgId: e.OrgID}
if err := bus.Dispatch(query); err != nil {
return nil, err
} else {
for _, ds := range query.Result {
if ds.IsDefault {
return ds, nil
}
}
for _, ds := range query.Result {
if ds.IsDefault {
return ds, nil
}
}
} else {
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgID}
if err := bus.Dispatch(query); err != nil {
return nil, err
} else {
return query.Result, nil
}
return query.Result, nil
}
return nil, errors.New("Could not find datasource id for " + dsName)
}
func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json {
func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Json {
for _, targetsObj := range panel.Get("targets").MustArray() {
target := simplejson.NewFromAny(targetsObj)
if target.Get("refId").MustString() == refId {
if target.Get("refId").MustString() == refID {
return target
}
}
return nil
}
func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
rawJson, err := in.MarshalJSON()
func copyJSON(in *simplejson.Json) (*simplejson.Json, error) {
rawJSON, err := in.MarshalJSON()
if err != nil {
return nil, err
}
return simplejson.NewJson(rawJson)
return simplejson.NewJson(rawJSON)
}
func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) {
func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, validateAlertFunc func(*m.Alert) bool) ([]*m.Alert, error) {
alerts := make([]*m.Alert, 0)
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj)
collapsedJson, collapsed := panel.CheckGet("collapsed")
collapsedJSON, collapsed := panel.CheckGet("collapsed")
// check if the panel is collapsed
if collapsed && collapsedJson.MustBool() {
if collapsed && collapsedJSON.MustBool() {
// extract alerts from sub panels for collapsed panels
als, err := e.GetAlertFromPanels(panel)
als, err := e.getAlertFromPanels(panel, validateAlertFunc)
if err != nil {
return nil, err
}
@@ -95,14 +97,14 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
continue
}
panelId, err := panel.Get("id").Int64()
panelID, err := panel.Get("id").Int64()
if err != nil {
return nil, fmt.Errorf("panel id is required. err %v", err)
}
// backward compatibility check, can be removed later
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
if hasEnabled && enabled.MustBool() == false {
if hasEnabled && !enabled.MustBool() {
continue
}
@@ -113,8 +115,8 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
alert := &m.Alert{
DashboardId: e.Dash.Id,
OrgId: e.OrgId,
PanelId: panelId,
OrgId: e.OrgID,
PanelId: panelID,
Id: jsonAlert.Get("id").MustInt64(),
Name: jsonAlert.Get("name").MustString(),
Handler: jsonAlert.Get("handler").MustInt64(),
@@ -126,11 +128,11 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
jsonCondition := simplejson.NewFromAny(condition)
jsonQuery := jsonCondition.Get("query")
queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
panelQuery := findPanelQueryByRefId(panel, queryRefId)
queryRefID := jsonQuery.Get("params").MustArray()[0].(string)
panelQuery := findPanelQueryByRefID(panel, queryRefID)
if panelQuery == nil {
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefID)
return nil, ValidationError{Reason: reason}
}
@@ -141,12 +143,13 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
dsName = panel.Get("datasource").MustString()
}
if datasource, err := e.lookupDatasourceId(dsName); err != nil {
datasource, err := e.lookupDatasourceID(dsName)
if err != nil {
return nil, err
} else {
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
}
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
if interval, err := panel.Get("interval").String(); err == nil {
panelQuery.Set("interval", interval)
}
@@ -162,21 +165,28 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
return nil, err
}
if alert.ValidToSave() {
alerts = append(alerts, alert)
} else {
if !validateAlertFunc(alert) {
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
return nil, m.ErrDashboardContainsInvalidAlertData
}
alerts = append(alerts, alert)
}
return alerts, nil
}
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
e.log.Debug("GetAlerts")
func validateAlertRule(alert *m.Alert) bool {
return alert.ValidToSave()
}
dashboardJson, err := copyJson(e.Dash.Data)
// GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
return e.extractAlerts(validateAlertRule)
}
func (e *DashAlertExtractor) extractAlerts(validateFunc func(alert *m.Alert) bool) ([]*m.Alert, error) {
dashboardJSON, err := copyJSON(e.Dash.Data)
if err != nil {
return nil, err
}
@@ -185,11 +195,11 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
// We extract alerts from rows to be backwards compatible
// with the old dashboard json model.
rows := dashboardJson.Get("rows").MustArray()
rows := dashboardJSON.Get("rows").MustArray()
if len(rows) > 0 {
for _, rowObj := range rows {
row := simplejson.NewFromAny(rowObj)
a, err := e.GetAlertFromPanels(row)
a, err := e.getAlertFromPanels(row, validateFunc)
if err != nil {
return nil, err
}
@@ -197,7 +207,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
alerts = append(alerts, a...)
}
} else {
a, err := e.GetAlertFromPanels(dashboardJson)
a, err := e.getAlertFromPanels(dashboardJSON, validateFunc)
if err != nil {
return nil, err
}
@@ -208,3 +218,10 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
return alerts, nil
}
// ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id
// in the first validation pass
func (e *DashAlertExtractor) ValidateAlerts() error {
_, err := e.extractAlerts(func(alert *m.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 })
return err
}
+21
View File
@@ -240,5 +240,26 @@ func TestAlertRuleExtraction(t *testing.T) {
So(len(alerts), ShouldEqual, 4)
})
})
Convey("Parse and validate dashboard without id and containing an alert", func() {
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1)
err = extractor.ValidateAlerts()
Convey("Should validate without error", func() {
So(err, ShouldBeNil)
})
Convey("Should fail on save", func() {
_, err := extractor.GetAlerts()
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
})
})
})
}
+12 -12
View File
@@ -87,17 +87,17 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
IsAlertContext: true,
}
if ref, err := context.GetDashboardUID(); err != nil {
ref, err := context.GetDashboardUID()
if err != nil {
return err
} else {
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
}
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
imagePath, err := renderer.RenderToPng(renderOpts)
if err != nil {
return err
} else {
context.ImageOnDiskPath = imagePath
}
context.ImageOnDiskPath = imagePath
context.ImagePublicUrl, err = uploader.Upload(context.Ctx, context.ImageOnDiskPath)
if err != nil {
@@ -117,12 +117,12 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
var result []Notifier
for _, notification := range query.Result {
if not, err := n.createNotifierFor(notification); err != nil {
not, err := n.createNotifierFor(notification)
if err != nil {
return nil, err
} else {
if not.ShouldNotify(context) {
result = append(result, not)
}
}
if not.ShouldNotify(context) {
result = append(result, not)
}
}
@@ -140,7 +140,7 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
var notifierFactories = make(map[string]*NotifierPlugin)
func RegisterNotifier(plugin *NotifierPlugin) {
notifierFactories[plugin.Type] = plugin
+4 -1
View File
@@ -72,7 +72,10 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
}
body, _ := bodyJSON.MarshalJSON()
body, err := bodyJSON.MarshalJSON()
if err != nil {
return err
}
cmd := &m.SendWebhookSync{
Url: this.Url,
+173
View File
@@ -0,0 +1,173 @@
package notifiers
import (
"bytes"
"io"
"mime/multipart"
"os"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/bus"
"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/services/alerting"
"github.com/grafana/grafana/pkg/setting"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "discord",
Name: "Discord",
Description: "Sends notifications to Discord",
Factory: NewDiscordNotifier,
OptionsTemplate: `
<h3 class="page-heading">Discord settings</h3>
<div class="gf-form">
<span class="gf-form-label width-14">Webhook URL</span>
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.url" placeholder="Discord webhook URL"></input>
</div>
`,
})
}
func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"}
}
return &DiscordNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
WebhookURL: url,
log: log.New("alerting.notifier.discord"),
}, nil
}
type DiscordNotifier struct {
NotifierBase
WebhookURL string
log log.Logger
}
func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL)
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
this.log.Error("Failed get rule link", "error", err)
return err
}
bodyJSON := simplejson.New()
bodyJSON.Set("username", "Grafana")
fields := make([]map[string]interface{}, 0)
for _, evt := range evalContext.EvalMatches {
fields = append(fields, map[string]interface{}{
"name": evt.Metric,
"value": evt.Value.FullString(),
"inline": true,
})
}
footer := map[string]interface{}{
"text": "Grafana v" + setting.BuildVersion,
"icon_url": "https://grafana.com/assets/img/fav32.png",
}
color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0)
embed := simplejson.New()
embed.Set("title", evalContext.GetNotificationTitle())
//Discord takes integer for color
embed.Set("color", color)
embed.Set("url", ruleUrl)
embed.Set("description", evalContext.Rule.Message)
embed.Set("type", "rich")
embed.Set("fields", fields)
embed.Set("footer", footer)
var image map[string]interface{}
var embeddedImage = false
if evalContext.ImagePublicUrl != "" {
image = map[string]interface{}{
"url": evalContext.ImagePublicUrl,
}
embed.Set("image", image)
} else {
image = map[string]interface{}{
"url": "attachment://graph.png",
}
embed.Set("image", image)
embeddedImage = true
}
bodyJSON.Set("embeds", []interface{}{embed})
json, _ := bodyJSON.MarshalJSON()
content_type := "application/json"
var body []byte
if embeddedImage {
var b bytes.Buffer
w := multipart.NewWriter(&b)
f, err := os.Open(evalContext.ImageOnDiskPath)
if err != nil {
this.log.Error("Can't open graph file", err)
return err
}
defer f.Close()
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
}
if _, err = fw.Write([]byte(string(json))); err != nil {
return err
}
fw, err = w.CreateFormFile("file", "graph.png")
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
w.Close()
body = b.Bytes()
content_type = w.FormDataContentType()
} else {
body = json
}
cmd := &m.SendWebhookSync{
Url: this.WebhookURL,
Body: string(body),
HttpMethod: "POST",
ContentType: content_type,
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send notification to Discord", "error", err)
return err
}
return nil
}
@@ -0,0 +1,52 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDiscordNotifier(t *testing.T) {
Convey("Telegram notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
_, err := NewDiscordNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("settings should trigger incident", func() {
json := `
{
"url": "https://web.hook/"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
not, err := NewDiscordNotifier(model)
discordNotifier := not.(*DiscordNotifier)
So(err, ShouldBeNil)
So(discordNotifier.Name, ShouldEqual, "discord_testing")
So(discordNotifier.Type, ShouldEqual, "discord")
So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/")
})
})
})
}
+1 -1
View File
@@ -111,7 +111,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
}
message := ""
if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
if evalContext.Rule.State != models.AlertStateOK { //don't add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
}
+1 -1
View File
@@ -90,7 +90,7 @@ func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
this.log.Error("Failed to send notification to LINE", "error", err, "body", body)
return err
}
+1 -1
View File
@@ -41,7 +41,7 @@ func init() {
}
var (
opsgenieAlertURL string = "https://api.opsgenie.com/v2/alerts"
opsgenieAlertURL = "https://api.opsgenie.com/v2/alerts"
)
func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+1 -1
View File
@@ -40,7 +40,7 @@ func init() {
}
var (
pagerdutyEventApiUrl string = "https://events.pagerduty.com/v2/enqueue"
pagerdutyEventApiUrl = "https://events.pagerduty.com/v2/enqueue"
)
func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+1 -1
View File
@@ -129,7 +129,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
}
message := this.Mention
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
}
image_url := ""
+1 -1
View File
@@ -13,7 +13,7 @@ func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "teams",
Name: "Microsoft Teams",
Description: "Sends notifications using Incomming Webhook connector to Microsoft Teams",
Description: "Sends notifications using Incoming Webhook connector to Microsoft Teams",
Factory: NewTeamsNotifier,
OptionsTemplate: `
<h3 class="page-heading">Teams settings</h3>
+10 -7
View File
@@ -3,13 +3,14 @@ package notifiers
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"os"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"io"
"mime/multipart"
"os"
)
const (
@@ -17,7 +18,7 @@ const (
)
var (
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
telegramApiUrl = "https://api.telegram.org/bot%s/%s"
)
func init() {
@@ -90,9 +91,8 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se
cmd, err := this.buildMessageInlineImage(evalContext)
if err == nil {
return cmd
} else {
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
}
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
}
return this.buildMessageLinkedImage(evalContext)
@@ -133,6 +133,9 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
}
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
return nil, err
}
metrics := generateMetricsMessage(evalContext)
message := generateImageCaption(evalContext, ruleUrl, metrics)
@@ -219,7 +222,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
var cmd *m.SendWebhookSync
if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
if evalContext.ImagePublicUrl == "" && this.UploadImage {
cmd = this.buildMessage(evalContext, true)
} else {
cmd = this.buildMessage(evalContext, false)
@@ -100,7 +100,7 @@ func TestTelegramNotifier(t *testing.T) {
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
})
Convey("Metrics should be skipped if they dont fit", func() {
Convey("Metrics should be skipped if they don't fit", func() {
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
@@ -83,27 +83,6 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
fields := make([]map[string]interface{}, 0)
fieldLimitCount := 4
for index, evt := range evalContext.EvalMatches {
fields = append(fields, map[string]interface{}{
"title": evt.Metric,
"value": evt.Value,
"short": true,
})
if index > fieldLimitCount {
break
}
}
if evalContext.Error != nil {
fields = append(fields, map[string]interface{}{
"title": "Error message",
"value": evalContext.Error.Error(),
"short": false,
})
}
messageType := evalContext.Rule.State
if evalContext.Rule.State == models.AlertStateAlerting { // translate 'Alerting' to 'CRITICAL' (Victorops analog)
messageType = AlertStateCritical
+1 -1
View File
@@ -16,7 +16,7 @@ type RuleReader interface {
type DefaultRuleReader struct {
sync.RWMutex
serverID string
//serverID string
serverPosition int
clusterSize int
log log.Logger
+2 -2
View File
@@ -56,7 +56,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
if err := bus.Dispatch(cmd); err != nil {
if err == m.ErrCannotChangeStateOnPausedAlert {
handler.log.Error("Cannot change state on alert thats pause", "error", err)
handler.log.Error("Cannot change state on alert that's paused", "error", err)
return err
}
@@ -77,7 +77,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
Text: "",
NewState: string(evalContext.Rule.State),
PrevState: string(evalContext.PrevAlertState),
Epoch: time.Now().Unix(),
Epoch: time.Now().UnixNano() / int64(time.Millisecond),
Data: annotationData,
}
+13 -13
View File
@@ -55,8 +55,8 @@ func (e ValidationError) Error() string {
}
var (
ValueFormatRegex = regexp.MustCompile("^\\d+")
UnitFormatRegex = regexp.MustCompile("\\w{1}$")
ValueFormatRegex = regexp.MustCompile(`^\d+`)
UnitFormatRegex = regexp.MustCompile(`\w{1}$`)
)
var unitMultiplier = map[string]int{
@@ -103,25 +103,25 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v)
if id, err := jsonModel.Get("id").Int64(); err != nil {
id, err := jsonModel.Get("id").Int64()
if err != nil {
return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else {
model.Notifications = append(model.Notifications, id)
}
model.Notifications = append(model.Notifications, id)
}
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
conditionModel := simplejson.NewFromAny(condition)
conditionType := conditionModel.Get("type").MustString()
if factory, exist := conditionFactories[conditionType]; !exist {
factory, exist := conditionFactories[conditionType]
if !exist {
return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else {
if queryCondition, err := factory(conditionModel, index); err != nil {
return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else {
model.Conditions = append(model.Conditions, queryCondition)
}
}
queryCondition, err := factory(conditionModel, index)
if err != nil {
return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
}
model.Conditions = append(model.Conditions, queryCondition)
}
if len(model.Conditions) == 0 {
@@ -133,7 +133,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
type ConditionFactory func(model *simplejson.Json, index int) (Condition, error)
var conditionFactories map[string]ConditionFactory = make(map[string]ConditionFactory)
var conditionFactories = make(map[string]ConditionFactory)
func RegisterCondition(typeName string, factory ConditionFactory) {
conditionFactories[typeName] = factory
+5 -5
View File
@@ -15,7 +15,7 @@ type SchedulerImpl struct {
func NewScheduler() Scheduler {
return &SchedulerImpl{
jobs: make(map[int64]*Job, 0),
jobs: make(map[int64]*Job),
log: log.New("alerting.scheduler"),
}
}
@@ -23,7 +23,7 @@ func NewScheduler() Scheduler {
func (s *SchedulerImpl) Update(rules []*Rule) {
s.log.Debug("Scheduling update", "ruleCount", len(rules))
jobs := make(map[int64]*Job, 0)
jobs := make(map[int64]*Job)
for i, rule := range rules {
var job *Job
@@ -58,7 +58,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
if job.OffsetWait && now%job.Offset == 0 {
job.OffsetWait = false
s.enque(job, execQueue)
s.enqueue(job, execQueue)
continue
}
@@ -66,13 +66,13 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
if job.Offset > 0 {
job.OffsetWait = true
} else {
s.enque(job, execQueue)
s.enqueue(job, execQueue)
}
}
}
}
func (s *SchedulerImpl) enque(job *Job, execQueue chan *Job) {
func (s *SchedulerImpl) enqueue(job *Job, execQueue chan *Job) {
s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id)
execQueue <- job
}
@@ -0,0 +1,281 @@
{
"title": "Influxdb",
"tags": [
"apa"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": "450px",
"panels": [
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
10
],
"type": "gt"
},
"query": {
"params": [
"B",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"frequency": "3s",
"handler": 1,
"name": "Influxdb",
"noDataState": "no_data",
"notifications": [
{
"id": 6
}
]
},
"alerting": {},
"aliasColors": {
"logins.count.count": "#890F02"
},
"bars": false,
"datasource": "InfluxDB",
"editable": true,
"error": false,
"fill": 1,
"grid": {},
"id": 1,
"interval": ">10s",
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"datacenter"
],
"type": "tag"
},
{
"params": [
"none"
],
"type": "fill"
}
],
"hide": false,
"measurement": "logins.count",
"policy": "default",
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
"rawQuery": true,
"refId": "B",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "count"
}
]
],
"tags": []
},
{
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"hide": true,
"measurement": "cpu",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
],
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "sum"
}
]
],
"tags": []
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 10
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"msResolution": false,
"ordering": "alphabetical",
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"editable": true,
"error": false,
"id": 2,
"isNew": true,
"limit": 10,
"links": [],
"show": "current",
"span": 2,
"stateFilter": [
"alerting"
],
"title": "Alert status",
"type": "alertlist"
}
],
"title": "Row"
}
],
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"now": true,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"schemaVersion": 13,
"version": 120,
"links": [],
"gnetId": null
}
@@ -279,4 +279,4 @@
"version": 120,
"links": [],
"gnetId": null
}
}
+5
View File
@@ -13,6 +13,7 @@ type ItemQuery struct {
OrgId int64 `json:"orgId"`
From int64 `json:"from"`
To int64 `json:"to"`
UserId int64 `json:"userId"`
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
@@ -63,6 +64,8 @@ type Item struct {
PrevState string `json:"prevState"`
NewState string `json:"newState"`
Epoch int64 `json:"epoch"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
Tags []string `json:"tags"`
Data *simplejson.Json `json:"data"`
@@ -80,6 +83,8 @@ type ItemDTO struct {
UserId int64 `json:"userId"`
NewState string `json:"newState"`
PrevState string `json:"prevState"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
Time int64 `json:"time"`
Text string `json:"text"`
RegionId int64 `json:"regionId"`
+30 -38
View File
@@ -7,60 +7,52 @@ import (
"path"
"time"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
type CleanUpService struct {
log log.Logger
Cfg *setting.Cfg `inject:""`
}
func NewCleanUpService() *CleanUpService {
return &CleanUpService{
log: log.New("cleanup"),
}
func init() {
registry.RegisterService(&CleanUpService{})
}
func (service *CleanUpService) Run(ctx context.Context) error {
service.log.Info("Initializing CleanUpService")
g, _ := errgroup.WithContext(ctx)
g.Go(func() error { return service.start(ctx) })
err := g.Wait()
service.log.Info("Stopped CleanUpService", "reason", err)
return err
func (srv *CleanUpService) Init() error {
srv.log = log.New("cleanup")
return nil
}
func (service *CleanUpService) start(ctx context.Context) error {
service.cleanUpTmpFiles()
func (srv *CleanUpService) Run(ctx context.Context) error {
srv.cleanUpTmpFiles()
ticker := time.NewTicker(time.Minute * 10)
for {
select {
case <-ticker.C:
service.cleanUpTmpFiles()
service.deleteExpiredSnapshots()
service.deleteExpiredDashboardVersions()
service.deleteOldLoginAttempts()
srv.cleanUpTmpFiles()
srv.deleteExpiredSnapshots()
srv.deleteExpiredDashboardVersions()
srv.deleteOldLoginAttempts()
case <-ctx.Done():
return ctx.Err()
}
}
}
func (service *CleanUpService) cleanUpTmpFiles() {
if _, err := os.Stat(setting.ImagesDir); os.IsNotExist(err) {
func (srv *CleanUpService) cleanUpTmpFiles() {
if _, err := os.Stat(srv.Cfg.ImagesDir); os.IsNotExist(err) {
return
}
files, err := ioutil.ReadDir(setting.ImagesDir)
files, err := ioutil.ReadDir(srv.Cfg.ImagesDir)
if err != nil {
service.log.Error("Problem reading image dir", "error", err)
srv.log.Error("Problem reading image dir", "error", err)
return
}
@@ -72,36 +64,36 @@ func (service *CleanUpService) cleanUpTmpFiles() {
}
for _, file := range toDelete {
fullPath := path.Join(setting.ImagesDir, file.Name())
fullPath := path.Join(srv.Cfg.ImagesDir, file.Name())
err := os.Remove(fullPath)
if err != nil {
service.log.Error("Failed to delete temp file", "file", file.Name(), "error", err)
srv.log.Error("Failed to delete temp file", "file", file.Name(), "error", err)
}
}
service.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
}
func (service *CleanUpService) deleteExpiredSnapshots() {
func (srv *CleanUpService) deleteExpiredSnapshots() {
cmd := m.DeleteExpiredSnapshotsCommand{}
if err := bus.Dispatch(&cmd); err != nil {
service.log.Error("Failed to delete expired snapshots", "error", err.Error())
srv.log.Error("Failed to delete expired snapshots", "error", err.Error())
} else {
service.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows)
srv.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows)
}
}
func (service *CleanUpService) deleteExpiredDashboardVersions() {
func (srv *CleanUpService) deleteExpiredDashboardVersions() {
cmd := m.DeleteExpiredVersionsCommand{}
if err := bus.Dispatch(&cmd); err != nil {
service.log.Error("Failed to delete expired dashboard versions", "error", err.Error())
srv.log.Error("Failed to delete expired dashboard versions", "error", err.Error())
} else {
service.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows)
srv.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows)
}
}
func (service *CleanUpService) deleteOldLoginAttempts() {
if setting.DisableBruteForceLoginProtection {
func (srv *CleanUpService) deleteOldLoginAttempts() {
if srv.Cfg.DisableBruteForceLoginProtection {
return
}
@@ -109,8 +101,8 @@ func (service *CleanUpService) deleteOldLoginAttempts() {
OlderThan: time.Now().Add(time.Minute * -10),
}
if err := bus.Dispatch(&cmd); err != nil {
service.log.Error("Problem deleting expired login attempts", "error", err.Error())
srv.log.Error("Problem deleting expired login attempts", "error", err.Error())
} else {
service.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows)
srv.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows)
}
}
+28 -5
View File
@@ -57,7 +57,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard
dash.Title = strings.TrimSpace(dash.Title)
@@ -103,6 +103,29 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, err
}
if validateBeforeSaveCmd.Result.IsParentFolderChanged {
folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
if err != nil {
return nil, err
}
return nil, models.ErrDashboardUpdateAccessDenied
}
}
if validateProvisionedDashboard {
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
err := bus.Dispatch(isDashboardProvisioned)
if err != nil {
return nil, err
}
if isDashboardProvisioned.Result {
return nil, models.ErrDashboardCannotSaveProvisionedDashboard
}
}
guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
if canSave, err := guard.CanSave(); err != nil || !canSave {
if err != nil {
@@ -148,7 +171,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto, true)
cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
if err != nil {
return nil, err
}
@@ -178,7 +201,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto, false)
cmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return nil, err
}
@@ -197,7 +220,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
}
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto, true)
cmd, err := dr.buildSaveDashboardCommand(dto, true, true)
if err != nil {
return nil, err
}
@@ -216,7 +239,7 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
}
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto, false)
cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
if err != nil {
return nil, err
}
@@ -14,7 +14,9 @@ import (
func TestDashboardService(t *testing.T) {
Convey("Dashboard service tests", t, func() {
service := dashboardServiceImpl{}
bus.ClearBusHandlers()
service := &dashboardServiceImpl{}
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
@@ -51,6 +53,12 @@ func TestDashboardService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
cmd.Result = false
return nil
})
@@ -72,12 +80,42 @@ func TestDashboardService(t *testing.T) {
dto.Dashboard.SetUid(tc.Uid)
dto.User = &models.SignedInUser{}
_, err := service.buildSaveDashboardCommand(dto, true)
_, err := service.buildSaveDashboardCommand(dto, true, false)
So(err, ShouldEqual, tc.Error)
}
})
Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
cmd.Result = true
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.SaveDashboard(dto)
So(provisioningValidated, ShouldBeTrue)
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
})
Convey("Should return validation error if alert data is invalid", func() {
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
cmd.Result = false
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return errors.New("error")
})
@@ -88,6 +126,80 @@ func TestDashboardService(t *testing.T) {
})
})
Convey("Save provisioned dashboard validation", func() {
dto := &SaveDashboardDTO{}
Convey("Should not return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
cmd.Result = true
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.SaveProvisionedDashboard(dto, nil)
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})
})
Convey("Import dashboard validation", func() {
dto := &SaveDashboardDTO{}
Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
cmd.Result = true
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.ImportDashboard(dto)
So(provisioningValidated, ShouldBeTrue)
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
})
})
Reset(func() {
guardian.New = origNewDashboardGuardian
})
+2 -2
View File
@@ -104,7 +104,7 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er
User: dr.user,
}
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return toFolderError(err)
}
@@ -141,7 +141,7 @@ func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.Upd
Overwrite: cmd.Overwrite,
}
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return toFolderError(err)
}
@@ -32,6 +32,7 @@ func TestFolderService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return models.ErrDashboardUpdateAccessDenied
})
@@ -92,6 +93,7 @@ func TestFolderService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
@@ -108,11 +110,19 @@ func TestFolderService(t *testing.T) {
return nil
})
provisioningValidated := false
bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
return nil
})
Convey("When creating folder should not return access denied error", func() {
err := service.CreateFolder(&models.CreateFolderCommand{
Title: "Folder",
})
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})
Convey("When updating folder should not return access denied error", func() {
@@ -121,6 +131,7 @@ func TestFolderService(t *testing.T) {
Title: "Folder",
})
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})
Convey("When deleting folder by uid should not return access denied error", func() {
+3 -15
View File
@@ -113,7 +113,7 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D
return false, err
}
// evalute team rules
// evaluate team rules
for _, p := range acl {
for _, ug := range teams {
if ug.Id == p.TeamId && p.Permission >= permission {
@@ -154,12 +154,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
// validate overridden permissions to be higher
for _, a := range acl {
for _, existingPerm := range existingPermissions {
// handle default permissions
if existingPerm.DashboardId == -1 {
existingPerm.DashboardId = g.dashId
}
if a.DashboardId == existingPerm.DashboardId {
if !existingPerm.Inherited {
continue
}
@@ -173,7 +168,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
return true, nil
}
return g.checkAcl(permission, acl)
return g.checkAcl(permission, existingPermissions)
}
// GetAcl returns dashboard acl
@@ -187,13 +182,6 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
return nil, err
}
for _, a := range query.Result {
// handle default permissions
if a.DashboardId == -1 {
a.DashboardId = g.dashId
}
}
g.acl = query.Result
return g.acl, nil
}
File diff suppressed because it is too large Load Diff
+256
View File
@@ -0,0 +1,256 @@
package guardian
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
type scenarioContext struct {
t *testing.T
orgRoleScenario string
permissionScenario string
g DashboardGuardian
givenUser *m.SignedInUser
givenDashboardID int64
givenPermissions []*m.DashboardAclInfoDTO
givenTeams []*m.Team
updatePermissions []*m.DashboardAcl
expectedFlags permissionFlags
callerFile string
callerLine int
}
type scenarioFunc func(c *scenarioContext)
func orgRoleScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc) {
user := &m.SignedInUser{
UserId: userID,
OrgId: orgID,
OrgRole: role,
}
guard := New(dashboardID, orgID, user)
sc := &scenarioContext{
t: t,
orgRoleScenario: desc,
givenUser: user,
givenDashboardID: dashboardID,
g: guard,
}
Convey(desc, func() {
fn(sc)
})
}
func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
bus.ClearBusHandlers()
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
if query.OrgId != sc.givenUser.OrgId {
sc.reportFailure("Invalid organization id for GetDashboardAclInfoListQuery", sc.givenUser.OrgId, query.OrgId)
}
if query.DashboardId != sc.givenDashboardID {
sc.reportFailure("Invalid dashboard id for GetDashboardAclInfoListQuery", sc.givenDashboardID, query.DashboardId)
}
query.Result = permissions
return nil
})
teams := []*m.Team{}
for _, p := range permissions {
if p.TeamId > 0 {
teams = append(teams, &m.Team{Id: p.TeamId})
}
}
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
if query.OrgId != sc.givenUser.OrgId {
sc.reportFailure("Invalid organization id for GetTeamsByUserQuery", sc.givenUser.OrgId, query.OrgId)
}
if query.UserId != sc.givenUser.UserId {
sc.reportFailure("Invalid user id for GetTeamsByUserQuery", sc.givenUser.UserId, query.UserId)
}
query.Result = teams
return nil
})
sc.permissionScenario = desc
sc.g = New(dashboardID, sc.givenUser.OrgId, sc.givenUser)
sc.givenDashboardID = dashboardID
sc.givenPermissions = permissions
sc.givenTeams = teams
Convey(desc, func() {
fn(sc)
})
}
type permissionType uint8
const (
USER permissionType = 1 << iota
TEAM
EDITOR
VIEWER
)
func (p permissionType) String() string {
names := map[uint8]string{
uint8(USER): "user",
uint8(TEAM): "team",
uint8(EDITOR): "editor role",
uint8(VIEWER): "viewer role",
}
return names[uint8(p)]
}
type permissionFlags uint8
const (
NO_ACCESS permissionFlags = 1 << iota
CAN_ADMIN
CAN_EDIT
CAN_SAVE
CAN_VIEW
FULL_ACCESS = CAN_ADMIN | CAN_EDIT | CAN_SAVE | CAN_VIEW
EDITOR_ACCESS = CAN_EDIT | CAN_SAVE | CAN_VIEW
VIEWER_ACCESS = CAN_VIEW
)
func (flag permissionFlags) canAdmin() bool {
return flag&CAN_ADMIN != 0
}
func (flag permissionFlags) canEdit() bool {
return flag&CAN_EDIT != 0
}
func (flag permissionFlags) canSave() bool {
return flag&CAN_SAVE != 0
}
func (flag permissionFlags) canView() bool {
return flag&CAN_VIEW != 0
}
func (flag permissionFlags) noAccess() bool {
return flag&(CAN_ADMIN|CAN_EDIT|CAN_SAVE|CAN_VIEW) == 0
}
func (f permissionFlags) String() string {
r := []string{}
if f.canAdmin() {
r = append(r, "admin")
}
if f.canEdit() {
r = append(r, "edit")
}
if f.canSave() {
r = append(r, "save")
}
if f.canView() {
r = append(r, "view")
}
if f.noAccess() {
r = append(r, "<no access>")
}
return strings.Join(r[:], ", ")
}
func (sc *scenarioContext) reportSuccess() {
So(true, ShouldBeTrue)
}
func (sc *scenarioContext) reportFailure(desc string, expected interface{}, actual interface{}) {
var buf bytes.Buffer
buf.WriteString("\n")
buf.WriteString(sc.orgRoleScenario)
buf.WriteString(" ")
buf.WriteString(sc.permissionScenario)
buf.WriteString("\n ")
buf.WriteString(desc)
buf.WriteString("\n")
buf.WriteString(fmt.Sprintf("Source test: %s:%d\n", sc.callerFile, sc.callerLine))
buf.WriteString(fmt.Sprintf("Expected: %v\n", expected))
buf.WriteString(fmt.Sprintf("Actual: %v\n", actual))
buf.WriteString("Context:")
buf.WriteString(fmt.Sprintf("\n Given user: orgRole=%s, id=%d, orgId=%d", sc.givenUser.OrgRole, sc.givenUser.UserId, sc.givenUser.OrgId))
buf.WriteString(fmt.Sprintf("\n Given dashboard id: %d", sc.givenDashboardID))
for i, p := range sc.givenPermissions {
r := "<nil>"
if p.Role != nil {
r = string(*p.Role)
}
buf.WriteString(fmt.Sprintf("\n Given permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
}
for i, t := range sc.givenTeams {
buf.WriteString(fmt.Sprintf("\n Given team (%d): id=%d", i, t.Id))
}
for i, p := range sc.updatePermissions {
r := "<nil>"
if p.Role != nil {
r = string(*p.Role)
}
buf.WriteString(fmt.Sprintf("\n Update permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
}
sc.t.Fatalf(buf.String())
}
func newCustomUserPermission(dashboardID int64, userID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, UserId: userID, Permission: permission}
}
func newDefaultUserPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return newCustomUserPermission(dashboardID, userID, permission)
}
func newCustomTeamPermission(dashboardID int64, teamID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, TeamId: teamID, Permission: permission}
}
func newDefaultTeamPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return newCustomTeamPermission(dashboardID, teamID, permission)
}
func newAdminRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &adminRole, Permission: permission}
}
func newEditorRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &editorRole, Permission: permission}
}
func newViewerRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &viewerRole, Permission: permission}
}
func toDto(acl *m.DashboardAcl) *m.DashboardAclInfoDTO {
return &m.DashboardAclInfoDTO{
OrgId: acl.OrgId,
DashboardId: acl.DashboardId,
UserId: acl.UserId,
TeamId: acl.TeamId,
Role: acl.Role,
Permission: acl.Permission,
PermissionName: acl.Permission.String(),
}
}
+16 -48
View File
@@ -7,51 +7,18 @@ package notifications
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"html/template"
"net"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/gomail.v2"
gomail "gopkg.in/mail.v2"
)
var mailQueue chan *Message
func initMailQueue() {
mailQueue = make(chan *Message, 10)
go processMailQueue()
}
func processMailQueue() {
for {
select {
case msg := <-mailQueue:
num, err := send(msg)
tos := strings.Join(msg.To, "; ")
info := ""
if err != nil {
if len(msg.Info) > 0 {
info = ", info: " + msg.Info
}
log.Error(4, fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err))
} else {
log.Trace(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info))
}
}
}
}
var addToMailQueue = func(msg *Message) {
mailQueue <- msg
}
func send(msg *Message) (int, error) {
dialer, err := createDialer()
func (ns *NotificationService) send(msg *Message) (int, error) {
dialer, err := ns.createDialer()
if err != nil {
return 0, err
}
@@ -75,8 +42,8 @@ func send(msg *Message) (int, error) {
return len(msg.To), nil
}
func createDialer() (*gomail.Dialer, error) {
host, port, err := net.SplitHostPort(setting.Smtp.Host)
func (ns *NotificationService) createDialer() (*gomail.Dialer, error) {
host, port, err := net.SplitHostPort(ns.Cfg.Smtp.Host)
if err != nil {
return nil, err
@@ -87,30 +54,31 @@ func createDialer() (*gomail.Dialer, error) {
}
tlsconfig := &tls.Config{
InsecureSkipVerify: setting.Smtp.SkipVerify,
InsecureSkipVerify: ns.Cfg.Smtp.SkipVerify,
ServerName: host,
}
if setting.Smtp.CertFile != "" {
cert, err := tls.LoadX509KeyPair(setting.Smtp.CertFile, setting.Smtp.KeyFile)
if ns.Cfg.Smtp.CertFile != "" {
cert, err := tls.LoadX509KeyPair(ns.Cfg.Smtp.CertFile, ns.Cfg.Smtp.KeyFile)
if err != nil {
return nil, fmt.Errorf("Could not load cert or key file. error: %v", err)
}
tlsconfig.Certificates = []tls.Certificate{cert}
}
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d := gomail.NewDialer(host, iPort, ns.Cfg.Smtp.User, ns.Cfg.Smtp.Password)
d.TLSConfig = tlsconfig
if setting.Smtp.EhloIdentity != "" {
d.LocalName = setting.Smtp.EhloIdentity
if ns.Cfg.Smtp.EhloIdentity != "" {
d.LocalName = ns.Cfg.Smtp.EhloIdentity
} else {
d.LocalName = setting.InstanceName
}
return d, nil
}
func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
if !setting.Smtp.Enabled {
func (ns *NotificationService) buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
if !ns.Cfg.Smtp.Enabled {
return nil, m.ErrSmtpNotEnabled
}
@@ -135,7 +103,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
subjectText, hasSubject := subjectData["value"]
if !hasSubject {
return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
return nil, fmt.Errorf("Missing subject in Template %s", cmd.Template)
}
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
@@ -154,7 +122,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
return &Message{
To: cmd.To,
From: fmt.Sprintf("%s <%s>", setting.Smtp.FromName, setting.Smtp.FromAddress),
From: fmt.Sprintf("%s <%s>", ns.Cfg.Smtp.FromName, ns.Cfg.Smtp.FromAddress),
Subject: subject,
Body: buffer.String(),
EmbededFiles: cmd.EmbededFiles,
+81 -37
View File
@@ -7,11 +7,13 @@ import (
"html/template"
"net/url"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -21,33 +23,46 @@ var tmplResetPassword = "reset_password.html"
var tmplSignUpStarted = "signup_started.html"
var tmplWelcomeOnSignUp = "welcome_on_signup.html"
func Init() error {
initMailQueue()
initWebhookQueue()
func init() {
registry.RegisterService(&NotificationService{})
}
bus.AddHandler("email", sendResetPasswordEmail)
bus.AddHandler("email", validateResetPasswordCode)
bus.AddHandler("email", sendEmailCommandHandler)
type NotificationService struct {
Bus bus.Bus `inject:""`
Cfg *setting.Cfg `inject:""`
bus.AddCtxHandler("email", sendEmailCommandHandlerSync)
mailQueue chan *Message
webhookQueue chan *Webhook
log log.Logger
}
bus.AddCtxHandler("webhook", SendWebhookSync)
func (ns *NotificationService) Init() error {
ns.log = log.New("notifications")
ns.mailQueue = make(chan *Message, 10)
ns.webhookQueue = make(chan *Webhook, 10)
bus.AddEventListener(signUpStartedHandler)
bus.AddEventListener(signUpCompletedHandler)
ns.Bus.AddHandler(ns.sendResetPasswordEmail)
ns.Bus.AddHandler(ns.validateResetPasswordCode)
ns.Bus.AddHandler(ns.sendEmailCommandHandler)
ns.Bus.AddCtxHandler(ns.sendEmailCommandHandlerSync)
ns.Bus.AddCtxHandler(ns.SendWebhookSync)
ns.Bus.AddEventListener(ns.signUpStartedHandler)
ns.Bus.AddEventListener(ns.signUpCompletedHandler)
mailTemplates = template.New("name")
mailTemplates.Funcs(template.FuncMap{
"Subject": subjectTemplateFunc,
})
templatePattern := filepath.Join(setting.StaticRootPath, setting.Smtp.TemplatesPattern)
templatePattern := filepath.Join(setting.StaticRootPath, ns.Cfg.Smtp.TemplatesPattern)
_, err := mailTemplates.ParseGlob(templatePattern)
if err != nil {
return err
}
if !util.IsEmail(setting.Smtp.FromAddress) {
if !util.IsEmail(ns.Cfg.Smtp.FromAddress) {
return errors.New("Invalid email address for SMTP from_address config")
}
@@ -58,14 +73,44 @@ func Init() error {
return nil
}
func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
return sendWebRequestSync(ctx, &Webhook{
Url: cmd.Url,
User: cmd.User,
Password: cmd.Password,
Body: cmd.Body,
HttpMethod: cmd.HttpMethod,
HttpHeader: cmd.HttpHeader,
func (ns *NotificationService) Run(ctx context.Context) error {
for {
select {
case webhook := <-ns.webhookQueue:
err := ns.sendWebRequestSync(context.Background(), webhook)
if err != nil {
ns.log.Error("Failed to send webrequest ", "error", err)
}
case msg := <-ns.mailQueue:
num, err := ns.send(msg)
tos := strings.Join(msg.To, "; ")
info := ""
if err != nil {
if len(msg.Info) > 0 {
info = ", info: " + msg.Info
}
ns.log.Error(fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err))
} else {
ns.log.Debug(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info))
}
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
return ns.sendWebRequestSync(ctx, &Webhook{
Url: cmd.Url,
User: cmd.User,
Password: cmd.Password,
Body: cmd.Body,
HttpMethod: cmd.HttpMethod,
HttpHeader: cmd.HttpHeader,
ContentType: cmd.ContentType,
})
}
@@ -74,8 +119,8 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string {
return ""
}
func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error {
message, err := buildEmailMessage(&m.SendEmailCommand{
func (ns *NotificationService) sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error {
message, err := ns.buildEmailMessage(&m.SendEmailCommand{
Data: cmd.Data,
Info: cmd.Info,
Template: cmd.Template,
@@ -88,25 +133,23 @@ func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSyn
return err
}
_, err = send(message)
_, err = ns.send(message)
return err
}
func sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
message, err := buildEmailMessage(cmd)
func (ns *NotificationService) sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
message, err := ns.buildEmailMessage(cmd)
if err != nil {
return err
}
addToMailQueue(message)
ns.mailQueue <- message
return nil
}
func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
return sendEmailCommandHandler(&m.SendEmailCommand{
func (ns *NotificationService) sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
return ns.sendEmailCommandHandler(&m.SendEmailCommand{
To: []string{cmd.User.Email},
Template: tmplResetPassword,
Data: map[string]interface{}{
@@ -116,7 +159,7 @@ func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
})
}
func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error {
func (ns *NotificationService) validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error {
login := getLoginForEmailCode(query.Code)
if login == "" {
return m.ErrInvalidEmailCode
@@ -135,18 +178,18 @@ func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error {
return nil
}
func signUpStartedHandler(evt *events.SignUpStarted) error {
func (ns *NotificationService) signUpStartedHandler(evt *events.SignUpStarted) error {
if !setting.VerifyEmailEnabled {
return nil
}
log.Info("User signup started: %s", evt.Email)
ns.log.Info("User signup started", "email", evt.Email)
if evt.Email == "" {
return nil
}
err := sendEmailCommandHandler(&m.SendEmailCommand{
err := ns.sendEmailCommandHandler(&m.SendEmailCommand{
To: []string{evt.Email},
Template: tmplSignUpStarted,
Data: map[string]interface{}{
@@ -155,6 +198,7 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
"SignUpUrl": setting.ToAbsUrl(fmt.Sprintf("signup/?email=%s&code=%s", url.QueryEscape(evt.Email), url.QueryEscape(evt.Code))),
},
})
if err != nil {
return err
}
@@ -163,12 +207,12 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
return bus.Dispatch(&emailSentCmd)
}
func signUpCompletedHandler(evt *events.SignUpCompleted) error {
if evt.Email == "" || !setting.Smtp.SendWelcomeEmailOnSignUp {
func (ns *NotificationService) signUpCompletedHandler(evt *events.SignUpCompleted) error {
if evt.Email == "" || !ns.Cfg.Smtp.SendWelcomeEmailOnSignUp {
return nil
}
return sendEmailCommandHandler(&m.SendEmailCommand{
return ns.sendEmailCommandHandler(&m.SendEmailCommand{
To: []string{evt.Email},
Template: tmplWelcomeOnSignUp,
Data: map[string]interface{}{
@@ -3,6 +3,7 @@ package notifications
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
@@ -17,25 +18,24 @@ type testTriggeredAlert struct {
func TestNotifications(t *testing.T) {
Convey("Given the notifications service", t, func() {
//bus.ClearBusHandlers()
setting.StaticRootPath = "../../../public/"
setting.Smtp.Enabled = true
setting.Smtp.TemplatesPattern = "emails/*.html"
setting.Smtp.FromAddress = "from@address.com"
setting.Smtp.FromName = "Grafana Admin"
err := Init()
ns := &NotificationService{}
ns.Bus = bus.New()
ns.Cfg = setting.NewCfg()
ns.Cfg.Smtp.Enabled = true
ns.Cfg.Smtp.TemplatesPattern = "emails/*.html"
ns.Cfg.Smtp.FromAddress = "from@address.com"
ns.Cfg.Smtp.FromName = "Grafana Admin"
err := ns.Init()
So(err, ShouldBeNil)
var sentMsg *Message
addToMailQueue = func(msg *Message) {
sentMsg = msg
}
Convey("When sending reset email password", func() {
err := sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}})
err := ns.sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}})
So(err, ShouldBeNil)
sentMsg := <-ns.mailQueue
So(sentMsg.Body, ShouldContainSubstring, "body")
So(sentMsg.Subject, ShouldEqual, "Reset your Grafana password - asd@asd.com")
So(sentMsg.Body, ShouldNotContainSubstring, "Subject")
@@ -12,23 +12,19 @@ import (
func TestEmailIntegrationTest(t *testing.T) {
SkipConvey("Given the notifications service", t, func() {
bus.ClearBusHandlers()
setting.StaticRootPath = "../../../public/"
setting.Smtp.Enabled = true
setting.Smtp.TemplatesPattern = "emails/*.html"
setting.Smtp.FromAddress = "from@address.com"
setting.Smtp.FromName = "Grafana Admin"
setting.BuildVersion = "4.0.0"
err := Init()
So(err, ShouldBeNil)
ns := &NotificationService{}
ns.Bus = bus.New()
ns.Cfg = setting.NewCfg()
ns.Cfg.Smtp.Enabled = true
ns.Cfg.Smtp.TemplatesPattern = "emails/*.html"
ns.Cfg.Smtp.FromAddress = "from@address.com"
ns.Cfg.Smtp.FromName = "Grafana Admin"
addToMailQueue = func(msg *Message) {
So(msg.From, ShouldEqual, "Grafana Admin <from@address.com>")
So(msg.To[0], ShouldEqual, "asdf@asdf.com")
ioutil.WriteFile("../../../tmp/test_email.html", []byte(msg.Body), 0777)
}
err := ns.Init()
So(err, ShouldBeNil)
Convey("When sending reset email password", func() {
cmd := &m.SendEmailCommand{
@@ -59,8 +55,13 @@ func TestEmailIntegrationTest(t *testing.T) {
Template: "alert_notification.html",
}
err := sendEmailCommandHandler(cmd)
err := ns.sendEmailCommandHandler(cmd)
So(err, ShouldBeNil)
sentMsg := <-ns.mailQueue
So(sentMsg.From, ShouldEqual, "Grafana Admin <from@address.com>")
So(sentMsg.To[0], ShouldEqual, "asdf@asdf.com")
ioutil.WriteFile("../../../tmp/test_email.html", []byte(sentMsg.Body), 0777)
})
})
}
+16 -39
View File
@@ -11,17 +11,17 @@ import (
"golang.org/x/net/context/ctxhttp"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
)
type Webhook struct {
Url string
User string
Password string
Body string
HttpMethod string
HttpHeader map[string]string
Url string
User string
Password string
Body string
HttpMethod string
HttpHeader map[string]string
ContentType string
}
var netTransport = &http.Transport{
@@ -37,32 +37,8 @@ var netClient = &http.Client{
Transport: netTransport,
}
var (
webhookQueue chan *Webhook
webhookLog log.Logger
)
func initWebhookQueue() {
webhookLog = log.New("notifications.webhook")
webhookQueue = make(chan *Webhook, 10)
go processWebhookQueue()
}
func processWebhookQueue() {
for {
select {
case webhook := <-webhookQueue:
err := sendWebRequestSync(context.Background(), webhook)
if err != nil {
webhookLog.Error("Failed to send webrequest ", "error", err)
}
}
}
}
func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod)
func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
ns.log.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod)
if webhook.HttpMethod == "" {
webhook.HttpMethod = http.MethodPost
@@ -73,8 +49,13 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
return err
}
request.Header.Add("Content-Type", "application/json")
if webhook.ContentType == "" {
webhook.ContentType = "application/json"
}
request.Header.Add("Content-Type", webhook.ContentType)
request.Header.Add("User-Agent", "Grafana")
if webhook.User != "" && webhook.Password != "" {
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
}
@@ -98,10 +79,6 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
return err
}
webhookLog.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body))
ns.log.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body))
return fmt.Errorf("Webhook response status %v", resp.Status)
}
var addToWebhookQueue = func(msg *Webhook) {
webhookQueue <- msg
}
@@ -58,7 +58,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
files, err := ioutil.ReadDir(cr.path)
if err != nil {
cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
cr.log.Error("can't read dashboard provisioning files from directory", "path", cr.path)
return dashboards, nil
}
@@ -69,7 +69,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
parsedDashboards, err := cr.parseConfigs(file)
if err != nil {
return nil, err
}
if len(parsedDashboards) > 0 {
@@ -8,9 +8,9 @@ import (
)
var (
simpleDashboardConfig string = "./test-configs/dashboards-from-disk"
oldVersion string = "./test-configs/version-0"
brokenConfigs string = "./test-configs/broken-configs"
simpleDashboardConfig = "./test-configs/dashboards-from-disk"
oldVersion = "./test-configs/version-0"
brokenConfigs = "./test-configs/broken-configs"
)
func TestDashboardsAsConfig(t *testing.T) {
@@ -10,19 +10,16 @@ import (
type DashboardProvisioner struct {
cfgReader *configReader
log log.Logger
ctx context.Context
}
func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
func NewDashboardProvisioner(configDirectory string) *DashboardProvisioner {
log := log.New("provisioning.dashboard")
d := &DashboardProvisioner{
cfgReader: &configReader{path: configDirectory, log: log},
log: log,
ctx: ctx,
}
err := d.Provision(ctx)
return d, err
return d
}
func (provider *DashboardProvisioner) Provision(ctx context.Context) error {
@@ -19,9 +19,9 @@ import (
)
var (
checkDiskForChangesInterval time.Duration = time.Second * 3
checkDiskForChangesInterval = time.Second * 3
ErrFolderNameMissing error = errors.New("Folder name missing")
ErrFolderNameMissing = errors.New("Folder name missing")
)
type fileReader struct {
@@ -235,7 +235,6 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardPr
func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
checkFilepath, err := filepath.EvalSymlinks(path)
if path != checkFilepath {
path = checkFilepath
fi, err := os.Lstat(checkFilepath)
if err != nil {
return nil, err
@@ -55,9 +55,6 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
dash.OrgId = cfg.OrgId
dash.Dashboard.OrgId = cfg.OrgId
dash.Dashboard.FolderId = folderId
if !cfg.Editable {
dash.Dashboard.Data.Set("editable", cfg.Editable)
}
if dash.Dashboard.Title == "" {
return nil, models.ErrDashboardTitleEmpty
@@ -19,7 +19,7 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error)
files, err := ioutil.ReadDir(path)
if err != nil {
cr.log.Error("cant read datasource provisioning files from directory", "path", path)
cr.log.Error("can't read datasource provisioning files from directory", "path", path)
return datasources, nil
}
@@ -11,14 +11,14 @@ import (
)
var (
logger log.Logger = log.New("fake.log")
oneDatasourcesConfig string = ""
twoDatasourcesConfig string = "./test-configs/two-datasources"
twoDatasourcesConfigPurgeOthers string = "./test-configs/insert-two-delete-two"
doubleDatasourcesConfig string = "./test-configs/double-default"
allProperties string = "./test-configs/all-properties"
versionZero string = "./test-configs/version-0"
brokenYaml string = "./test-configs/broken-yaml"
logger log.Logger = log.New("fake.log")
twoDatasourcesConfig = "./test-configs/two-datasources"
twoDatasourcesConfigPurgeOthers = "./test-configs/insert-two-delete-two"
doubleDatasourcesConfig = "./test-configs/double-default"
allProperties = "./test-configs/all-properties"
versionZero = "./test-configs/version-0"
brokenYaml = "./test-configs/broken-yaml"
fakeRepo *fakeRepository
)
+22 -16
View File
@@ -2,34 +2,40 @@ package provisioning
import (
"context"
"fmt"
"path"
"path/filepath"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
ini "gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/setting"
)
func Init(ctx context.Context, homePath string, cfg *ini.File) error {
provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath)
func init() {
registry.RegisterService(&ProvisioningService{})
}
datasourcePath := path.Join(provisioningPath, "datasources")
type ProvisioningService struct {
Cfg *setting.Cfg `inject:""`
}
func (ps *ProvisioningService) Init() error {
datasourcePath := path.Join(ps.Cfg.ProvisioningPath, "datasources")
if err := datasources.Provision(datasourcePath); err != nil {
return err
}
dashboardPath := path.Join(provisioningPath, "dashboards")
_, err := dashboards.Provision(ctx, dashboardPath)
if err != nil {
return err
return fmt.Errorf("Datasource provisioning error: %v", err)
}
return nil
}
func makeAbsolute(path string, root string) string {
if filepath.IsAbs(path) {
return path
func (ps *ProvisioningService) Run(ctx context.Context) error {
dashboardPath := path.Join(ps.Cfg.ProvisioningPath, "dashboards")
dashProvisioner := dashboards.NewDashboardProvisioner(dashboardPath)
if err := dashProvisioner.Provision(ctx); err != nil {
return err
}
return filepath.Join(root, path)
<-ctx.Done()
return ctx.Err()
}
+13 -3
View File
@@ -5,13 +5,23 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
)
func Init() {
bus.AddHandler("search", searchHandler)
func init() {
registry.RegisterService(&SearchService{})
}
func searchHandler(query *Query) error {
type SearchService struct {
Bus bus.Bus `inject:""`
}
func (s *SearchService) Init() error {
s.Bus.AddHandler(s.searchHandler)
return nil
}
func (s *SearchService) searchHandler(query *Query) error {
dashQuery := FindPersistedDashboardsQuery{
Title: query.Title,
SignedInUser: query.SignedInUser,
+2 -1
View File
@@ -12,6 +12,7 @@ func TestSearch(t *testing.T) {
Convey("Given search query", t, func() {
query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}}
ss := &SearchService{}
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
query.Result = HitList{
@@ -35,7 +36,7 @@ func TestSearch(t *testing.T) {
})
Convey("That is empty", func() {
err := searchHandler(&query)
err := ss.searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return sorted results", func() {
+1 -6
View File
@@ -23,12 +23,7 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
return inTransaction(func(sess *DBSession) error {
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
if err != nil {
return err
}
return nil
return err
})
}
@@ -21,7 +21,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
}
err := GetAlertNotifications(cmd)
fmt.Printf("errror %v", err)
fmt.Printf("error %v", err)
So(err, ShouldBeNil)
So(cmd.Result, ShouldBeNil)
})
+34 -20
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
@@ -17,18 +18,24 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
return inTransaction(func(sess *DBSession) error {
tags := models.ParseTagPairs(item.Tags)
item.Tags = models.JoinTagPairs(tags)
item.Created = time.Now().UnixNano() / int64(time.Millisecond)
item.Updated = item.Created
if item.Epoch == 0 {
item.Epoch = item.Created
}
if _, err := sess.Table("annotation").Insert(item); err != nil {
return err
}
if item.Tags != nil {
if tags, err := r.ensureTagsExist(sess, tags); err != nil {
tags, err := r.ensureTagsExist(sess, tags)
if err != nil {
return err
} else {
for _, tag := range tags {
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
return err
}
}
for _, tag := range tags {
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
return err
}
}
}
@@ -79,6 +86,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
return errors.New("Annotation not found")
}
existing.Updated = time.Now().UnixNano() / int64(time.Millisecond)
existing.Epoch = item.Epoch
existing.Text = item.Text
if item.RegionId != 0 {
@@ -86,27 +94,24 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
}
if item.Tags != nil {
if tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)); err != nil {
tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags))
if err != nil {
return err
} else {
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
}
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
return err
}
for _, tag := range tags {
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
return err
}
for _, tag := range tags {
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
return err
}
}
}
}
existing.Tags = item.Tags
if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
return err
}
return nil
_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing)
return err
})
}
@@ -127,6 +132,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
annotation.text,
annotation.tags,
annotation.data,
annotation.created,
annotation.updated,
usr.email,
usr.login,
alert.name as alert_name
@@ -164,6 +171,11 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
params = append(params, query.PanelId)
}
if query.UserId != 0 {
sql.WriteString(` AND annotation.user_id = ?`)
params = append(params, query.UserId)
}
if query.From > 0 && query.To > 0 {
sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
params = append(params, query.From, query.To)
@@ -171,6 +183,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
if query.Type == "alert" {
sql.WriteString(` AND annotation.alert_id > 0`)
} else if query.Type == "annotation" {
sql.WriteString(` AND annotation.alert_id = 0`)
}
if len(query.Tags) > 0 {
@@ -202,7 +216,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
}
if query.Limit == 0 {
query.Limit = 10
query.Limit = 100
}
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
+11
View File
@@ -79,6 +79,12 @@ func TestAnnotations(t *testing.T) {
Convey("Can read tags", func() {
So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"})
})
Convey("Has created and updated values", func() {
So(items[0].Created, ShouldBeGreaterThan, 0)
So(items[0].Updated, ShouldBeGreaterThan, 0)
So(items[0].Updated, ShouldEqual, items[0].Created)
})
})
Convey("Can query for annotation by id", func() {
@@ -231,6 +237,10 @@ func TestAnnotations(t *testing.T) {
So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"})
So(items[0].Text, ShouldEqual, "something new")
})
Convey("Updated time has increased", func() {
So(items[0].Updated, ShouldBeGreaterThan, items[0].Created)
})
})
Convey("Can delete annotation", func() {
@@ -246,6 +256,7 @@ func TestAnnotations(t *testing.T) {
annotationId := items[0].Id
err = repo.Delete(&annotations.DeleteParams{Id: annotationId})
So(err, ShouldBeNil)
items, err = repo.Find(query)
So(err, ShouldBeNil)
+2 -2
View File
@@ -55,7 +55,7 @@ func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrInvalidApiKey
}
@@ -69,7 +69,7 @@ func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrInvalidApiKey
}
+47 -19
View File
@@ -24,6 +24,7 @@ func init() {
bus.AddHandler("sql", GetDashboardPermissionsForUser)
bus.AddHandler("sql", GetDashboardsBySlug)
bus.AddHandler("sql", ValidateDashboardBeforeSave)
bus.AddHandler("sql", HasEditPermissionInFolders)
}
var generateNewUid func() string = util.GenerateShortUid
@@ -63,7 +64,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
if existing.PluginId != "" && !cmd.Overwrite {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
}
@@ -77,7 +78,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
}
parentVersion := dash.Version
affectedRows := int64(0)
var affectedRows int64
var err error
if dash.Id == 0 {
@@ -172,7 +173,7 @@ func GetDashboard(query *m.GetDashboardQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrDashboardNotFound
}
@@ -308,7 +309,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
has, err := sess.Get(&dashboard)
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrDashboardNotFound
}
@@ -347,12 +348,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
err := x.In("id", query.DashboardIds).Find(&dashboards)
query.Result = dashboards
if err != nil {
return err
}
return nil
return err
}
// GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
@@ -431,12 +427,7 @@ func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
query.Result = dashboards
if err != nil {
return err
}
return nil
return err
}
type DashboardSlugDTO struct {
@@ -451,7 +442,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
if err != nil {
return err
} else if exists == false {
} else if !exists {
return m.ErrDashboardNotFound
}
@@ -479,7 +470,7 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
if err != nil {
return err
} else if exists == false {
} else if !exists {
return m.ErrDashboardNotFound
}
@@ -544,6 +535,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
dash.SetId(existingByUid.Id)
dash.SetUid(existingByUid.Uid)
existing = existingByUid
if !dash.IsFolder {
cmd.Result.IsParentFolderChanged = true
}
}
if (existing.IsFolder && !dash.IsFolder) ||
@@ -551,6 +546,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
return m.ErrDashboardTypeMismatch
}
if !dash.IsFolder && dash.FolderId != existing.FolderId {
cmd.Result.IsParentFolderChanged = true
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
@@ -561,7 +560,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
if existing.PluginId != "" && !cmd.Overwrite {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
@@ -586,6 +585,10 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
return m.ErrDashboardFolderWithSameNameAsDashboard
}
if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) {
cmd.Result.IsParentFolderChanged = true
}
if cmd.Overwrite {
dash.SetId(existing.Id)
dash.SetUid(existing.Uid)
@@ -599,6 +602,7 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
}
func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
cmd.Result = &m.ValidateDashboardBeforeSaveResult{}
return inTransaction(func(sess *DBSession) error {
if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
return err
@@ -611,3 +615,27 @@ func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err
return nil
})
}
func HasEditPermissionInFolders(query *m.HasEditPermissionInFoldersQuery) error {
if query.SignedInUser.HasRole(m.ROLE_EDITOR) {
query.Result = true
return nil
}
builder := &SqlBuilder{}
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgId, dialect.BooleanStr(true))
builder.writeDashboardPermissionFilter(query.SignedInUser, m.PERMISSION_EDIT)
type folderCount struct {
Count int64
}
resp := make([]*folderCount, 0)
if err := x.Sql(builder.GetSqlString(), builder.params...).Find(&resp); err != nil {
return err
}
query.Result = len(resp) > 0 && resp[0].Count > 0
return nil
}
+7 -6
View File
@@ -35,10 +35,8 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
// Update dashboard HasAcl flag
dashboard := m.Dashboard{HasAcl: true}
if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
_, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard)
return err
})
}
@@ -69,7 +67,8 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
'' as title,
'' as slug,
'' as uid,` +
falseStr + ` AS is_folder
falseStr + ` AS is_folder,` +
falseStr + ` AS inherited
FROM dashboard_acl as da
WHERE da.dashboard_id = -1`
query.Result = make([]*m.DashboardAclInfoDTO, 0)
@@ -92,10 +91,12 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
u.login AS user_login,
u.email AS user_email,
ug.name AS team,
ug.email AS team_email,
d.title,
d.slug,
d.uid,
d.is_folder
d.is_folder,
CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited
FROM dashboard as d
LEFT JOIN dashboard folder on folder.id = d.folder_id
LEFT JOIN dashboard_acl AS da ON
@@ -26,6 +26,22 @@ func TestDashboardAclDataAccess(t *testing.T) {
})
Convey("Given dashboard folder with default permissions", func() {
Convey("When reading folder acl should include default acl", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeFalse)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeFalse)
})
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
@@ -36,8 +52,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeTrue)
})
})
@@ -94,7 +112,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, childDash.Id)
So(query.Result[1].Inherited, ShouldBeFalse)
})
})
})
@@ -118,9 +138,12 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeTrue)
So(query.Result[2].DashboardId, ShouldEqual, childDash.Id)
So(query.Result[2].Inherited, ShouldBeFalse)
})
})
@@ -131,6 +154,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
@@ -209,8 +233,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeFalse)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeFalse)
})
})
})
+52 -1
View File
@@ -221,7 +221,6 @@ func TestDashboardFolderDataAccess(t *testing.T) {
})
Convey("Given two dashboard folders", func() {
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
insertTestDashboard("folder in another org", 2, 0, true, "prod")
@@ -264,6 +263,15 @@ func TestDashboardFolderDataAccess(t *testing.T) {
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_ADMIN)
})
Convey("should have edit permission in folders", func() {
query := &m.HasEditPermissionInFoldersQuery{
SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: m.ROLE_ADMIN},
}
err := HasEditPermissionInFolders(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeTrue)
})
})
Convey("Editor users", func() {
@@ -310,6 +318,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
So(query.Result[0].Id, ShouldEqual, folder2.Id)
})
Convey("should have edit permission in folders", func() {
query := &m.HasEditPermissionInFoldersQuery{
SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgId: 1, OrgRole: m.ROLE_EDITOR},
}
err := HasEditPermissionInFolders(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeTrue)
})
})
Convey("Viewer users", func() {
@@ -353,6 +369,41 @@ func TestDashboardFolderDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Id, ShouldEqual, folder1.Id)
})
Convey("should not have edit permission in folders", func() {
query := &m.HasEditPermissionInFoldersQuery{
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
}
err := HasEditPermissionInFolders(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse)
})
Convey("and admin permission is given for user with org role viewer in one dashboard folder", func() {
testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_ADMIN})
Convey("should have edit permission in folders", func() {
query := &m.HasEditPermissionInFoldersQuery{
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
}
err := HasEditPermissionInFolders(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeTrue)
})
})
Convey("and edit permission is given for user with org role viewer in one dashboard folder", func() {
testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT})
Convey("should have edit permission in folders", func() {
query := &m.HasEditPermissionInFoldersQuery{
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
}
err := HasEditPermissionInFolders(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeTrue)
})
})
})
})
})
@@ -8,6 +8,7 @@ import (
func init() {
bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
bus.AddHandler("sql", SaveProvisionedDashboard)
bus.AddHandler("sql", GetProvisionedDataByDashboardId)
}
type DashboardExtras struct {
@@ -17,6 +18,19 @@ type DashboardExtras struct {
Value string
}
func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error {
result := &models.DashboardProvisioning{}
exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result)
if err != nil {
return err
}
cmd.Result = exist
return nil
}
func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {
err := saveDashboard(sess, cmd.DashboardCmd)
@@ -50,6 +50,23 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(query.Result[0].DashboardId, ShouldEqual, dashId)
So(query.Result[0].Updated, ShouldEqual, now.Unix())
})
Convey("Can query for one provisioned dashboard", func() {
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
err := GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeTrue)
})
Convey("Can query for none provisioned dashboard", func() {
query := &models.IsDashboardProvisionedQuery{DashboardId: 3000}
err := GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse)
})
})
})
}
@@ -19,7 +19,6 @@ func TestIntegratedDashboardService(t *testing.T) {
var testOrgId int64 = 1
Convey("Given saved folders and dashboards in organization A", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
@@ -28,6 +27,11 @@ func TestIntegratedDashboardService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
cmd.Result = false
return nil
})
savedFolder := saveTestFolder("Saved folder", testOrgId)
savedDashInFolder := saveTestDashboard("Saved dash in folder", testOrgId, savedFolder.Id)
saveTestDashboard("Other saved dash in folder", testOrgId, savedFolder.Id)
@@ -74,7 +78,7 @@ func TestIntegratedDashboardService(t *testing.T) {
Convey("Given organization B", func() {
var otherOrgId int64 = 2
Convey("When saving a dashboard with id that are saved in organization A", func() {
Convey("When creating a dashboard with same id as dashboard in organization A", func() {
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -93,7 +97,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
Convey("When saving a dashboard with uid that are saved in organization A", func() {
Convey("When creating a dashboard with same uid as dashboard in organization A", func() {
var otherOrgId int64 = 2
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
@@ -106,7 +110,7 @@ func TestIntegratedDashboardService(t *testing.T) {
res := callSaveWithResult(cmd)
Convey("It should create dashboard in other organization", func() {
Convey("It should create a new dashboard in organization B", func() {
So(res, ShouldNotBeNil)
query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
@@ -126,7 +130,7 @@ func TestIntegratedDashboardService(t *testing.T) {
permissionScenario("Given user has no permission to save", false, func(sc *dashboardPermissionScenarioContext) {
Convey("When trying to create a new dashboard in the General folder", func() {
Convey("When creating a new dashboard in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -138,7 +142,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
Convey("It should create dashboard guardian for General Folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -148,7 +152,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to create a new dashboard in other folder", func() {
Convey("When creating a new dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -161,7 +165,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and rsult in access denied error", func() {
Convey("It should create dashboard guardian for other folder with correct arguments and rsult in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -171,7 +175,54 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update a dashboard by existing id in the General folder", func() {
Convey("When creating a new dashboard by existing title in folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedDashInFolder.Title,
}),
FolderId: savedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When creating a new dashboard by existing uid in folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "New dash",
}),
FolderId: savedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When updating a dashboard by existing id in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -185,7 +236,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -195,7 +246,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update a dashboard by existing id in other folder", func() {
Convey("When updating a dashboard by existing id in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -209,7 +260,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -218,6 +269,102 @@ func TestIntegratedDashboardService(t *testing.T) {
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing id to other folder from General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Dash",
}),
FolderId: otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing id to the General folder from other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "Dash",
}),
FolderId: 0,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing uid to other folder from General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInGeneralFolder.Uid,
"title": "Dash",
}),
FolderId: otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing uid to the General folder from other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Dash",
}),
FolderId: 0,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
})
// Given user has permission to save
@@ -668,7 +815,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing folder to a dashboard using id", func() {
Convey("When updating existing folder to a dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -687,7 +834,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing dashboard to a folder using id", func() {
Convey("When updating existing dashboard to a folder using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -706,7 +853,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing folder to a dashboard using uid", func() {
Convey("When updating existing folder to a dashboard using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -725,7 +872,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing dashboard to a folder using uid", func() {
Convey("When updating existing dashboard to a folder using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -744,7 +891,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing folder to a dashboard using title", func() {
Convey("When updating existing folder to a dashboard using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -762,7 +909,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing dashboard to a folder using title", func() {
Convey("When updating existing dashboard to a folder using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -850,23 +997,6 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error {
return err
}
func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(mock)
sc := &scenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
func saveTestDashboard(title string, orgId int64, folderId int64) *models.Dashboard {
cmd := models.SaveDashboardCommand{
OrgId: orgId,
+1 -1
View File
@@ -80,7 +80,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrDashboardSnapshotNotFound
}
+1 -2
View File
@@ -104,9 +104,8 @@ func TestDashboardDataAccess(t *testing.T) {
timesCalled += 1
if timesCalled <= 2 {
return savedDash.Uid
} else {
return util.GenerateShortUid()
}
return util.GenerateShortUid()
}
cmd := m.SaveDashboardCommand{
OrgId: 1,
@@ -90,4 +90,29 @@ func addAnnotationMig(mg *Migrator) {
Sqlite(updateTextFieldSql).
Postgres(updateTextFieldSql).
Mysql(updateTextFieldSql))
//
// Add a 'created' & 'updated' column
//
mg.AddMigration("Add created time to annotation table", NewAddColumnMigration(table, &Column{
Name: "created", Type: DB_BigInt, Nullable: true, Default: "0",
}))
mg.AddMigration("Add updated time to annotation table", NewAddColumnMigration(table, &Column{
Name: "updated", Type: DB_BigInt, Nullable: true, Default: "0",
}))
mg.AddMigration("Add index for created in annotation table", NewAddIndexMigration(table, &Index{
Cols: []string{"org_id", "created"}, Type: IndexType,
}))
mg.AddMigration("Add index for updated in annotation table", NewAddIndexMigration(table, &Index{
Cols: []string{"org_id", "updated"}, Type: IndexType,
}))
//
// Convert epoch saved as seconds to miliseconds
//
updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
mg.AddMigration("Convert existing annotations from seconds to milliseconds", new(RawSqlMigration).
Sqlite(updateEpochSql).
Postgres(updateEpochSql).
Mysql(updateEpochSql))
}
@@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) {
addDashboardAclMigrations(mg)
addTagMigration(mg)
addLoginAttemptMigrations(mg)
addUserAuthMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
@@ -8,11 +8,8 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
. "github.com/smartystreets/goconvey/convey"
//"github.com/grafana/grafana/pkg/log"
)
var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"}
func TestMigrations(t *testing.T) {
testDBs := []sqlutil.TestDB{
sqlutil.TestDB_Sqlite3,
@@ -30,7 +27,7 @@ func TestMigrations(t *testing.T) {
sqlutil.CleanDB(x)
has, err := x.SQL(sql).Get(&r)
_, err = x.SQL(sql).Get(&r)
So(err, ShouldNotBeNil)
mg := NewMigrator(x)
@@ -39,7 +36,7 @@ func TestMigrations(t *testing.T) {
err = mg.Start()
So(err, ShouldBeNil)
has, err = x.SQL(sql).Get(&r)
has, err := x.SQL(sql).Get(&r)
So(err, ShouldBeNil)
So(has, ShouldBeTrue)
expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this
+32 -31
View File
@@ -2,37 +2,38 @@ package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addStatsMigrations(mg *Migrator) {
statTable := Table{
Name: "stat",
Columns: []*Column{
{Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "metric", Type: DB_Varchar, Length: 20, Nullable: false},
{Name: "type", Type: DB_Int, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"metric"}, Type: UniqueIndex},
},
}
// create table
mg.AddMigration("create stat table", NewAddTableMigration(statTable))
// create indices
mg.AddMigration("add index stat.metric", NewAddIndexMigration(statTable, statTable.Indices[0]))
statValue := Table{
Name: "stat_value",
Columns: []*Column{
{Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "value", Type: DB_Double, Nullable: false},
{Name: "time", Type: DB_DateTime, Nullable: false},
},
}
// create table
mg.AddMigration("create stat_value table", NewAddTableMigration(statValue))
}
// commented out because of the deadcode CI check
//func addStatsMigrations(mg *Migrator) {
// statTable := Table{
// Name: "stat",
// Columns: []*Column{
// {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
// {Name: "metric", Type: DB_Varchar, Length: 20, Nullable: false},
// {Name: "type", Type: DB_Int, Nullable: false},
// },
// Indices: []*Index{
// {Cols: []string{"metric"}, Type: UniqueIndex},
// },
// }
//
// // create table
// mg.AddMigration("create stat table", NewAddTableMigration(statTable))
//
// // create indices
// mg.AddMigration("add index stat.metric", NewAddIndexMigration(statTable, statTable.Indices[0]))
//
// statValue := Table{
// Name: "stat_value",
// Columns: []*Column{
// {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
// {Name: "value", Type: DB_Double, Nullable: false},
// {Name: "time", Type: DB_DateTime, Nullable: false},
// },
// }
//
// // create table
// mg.AddMigration("create stat_value table", NewAddTableMigration(statValue))
//}
func addTestDataMigrations(mg *Migrator) {
testData := Table{
@@ -0,0 +1,29 @@
package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addUserAuthMigrations(mg *Migrator) {
userAuthV1 := Table{
Name: "user_auth",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "user_id", Type: DB_BigInt, Nullable: false},
{Name: "auth_module", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "auth_id", Type: DB_NVarchar, Length: 100, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"auth_module", "auth_id"}},
},
}
// create table
mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1))
// add indices
addTableIndicesMigrations(mg, "v1", userAuthV1)
mg.AddMigration("alter user_auth.auth_id to length 190", new(RawSqlMigration).
Sqlite("SELECT 0 WHERE 0;").
Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(190);").
Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(190);"))
}
+2 -4
View File
@@ -84,8 +84,7 @@ func (db *BaseDialect) DateTimeFunc(value string) string {
}
func (b *BaseDialect) CreateTableSql(table *Table) string {
var sql string
sql = "CREATE TABLE IF NOT EXISTS "
sql := "CREATE TABLE IF NOT EXISTS "
sql += b.dialect.Quote(table.Name) + " (\n"
pkList := table.PrimaryKeys
@@ -162,8 +161,7 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
quote := db.dialect.Quote
var name string
name = index.XName(tableName)
name := index.XName(tableName)
return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
}
+2 -3
View File
@@ -1,7 +1,6 @@
package migrator
import (
"fmt"
"strings"
)
@@ -113,7 +112,7 @@ func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
func (m *DropIndexMigration) Sql(dialect Dialect) string {
if m.index.Name == "" {
m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_"))
m.index.Name = strings.Join(m.index.Cols, "_")
}
return dialect.DropIndexSql(m.tableName, m.index)
}
@@ -180,7 +179,7 @@ type CopyTableDataMigration struct {
targetTable string
sourceCols []string
targetCols []string
colMap map[string]string
//colMap map[string]string
}
func NewCopyTableDataMigration(targetTable string, sourceTable string, colMap map[string]string) *CopyTableDataMigration {
+4 -6
View File
@@ -97,17 +97,15 @@ func (mg *Migrator) Start() error {
mg.Logger.Debug("Executing", "sql", sql)
err := mg.inTransaction(func(sess *xorm.Session) error {
if err := mg.exec(m, sess); err != nil {
err := mg.exec(m, sess)
if err != nil {
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
record.Error = err.Error()
sess.Insert(&record)
return err
} else {
record.Success = true
sess.Insert(&record)
}
record.Success = true
sess.Insert(&record)
return nil
})
@@ -66,8 +66,8 @@ func (db *Mysql) SqlType(c *Column) string {
res = c.Type
}
var hasLen1 bool = (c.Length > 0)
var hasLen2 bool = (c.Length2 > 0)
var hasLen1 = (c.Length > 0)
var hasLen2 = (c.Length2 > 0)
if res == DB_BigInt && !hasLen1 && !hasLen2 {
c.Length = 20
@@ -45,9 +45,8 @@ func (b *Postgres) Default(col *Column) string {
if col.Type == DB_Bool {
if col.Default == "0" {
return "FALSE"
} else {
return "TRUE"
}
return "TRUE"
}
return col.Default
}
@@ -92,8 +91,8 @@ func (db *Postgres) SqlType(c *Column) string {
res = t
}
var hasLen1 bool = (c.Length > 0)
var hasLen2 bool = (c.Length2 > 0)
var hasLen1 = (c.Length > 0)
var hasLen2 = (c.Length2 > 0)
if hasLen2 {
res += "(" + strconv.Itoa(c.Length) + "," + strconv.Itoa(c.Length2) + ")"
} else if hasLen1 {
+1 -1
View File
@@ -46,7 +46,7 @@ type Index struct {
func (index *Index) XName(tableName string) string {
if index.Name == "" {
index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_"))
index.Name = strings.Join(index.Cols, "_")
}
if !strings.HasPrefix(index.Name, "UQE_") &&
+3
View File
@@ -22,6 +22,9 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
}
_, err := x.Insert(&playlist)
if err != nil {
return err
}
playlistItems := make([]m.PlaylistItem, 0)
for _, item := range cmd.Items {
+30 -28
View File
@@ -36,7 +36,7 @@ func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
has, err := x.Get(&pluginSetting)
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrPluginSettingNotFound
}
query.Result = &pluginSetting
@@ -48,6 +48,9 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
var pluginSetting m.PluginSetting
exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting)
if err != nil {
return err
}
sess.UseBool("enabled")
sess.UseBool("pinned")
if !exists {
@@ -72,34 +75,33 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
_, err = sess.Insert(&pluginSetting)
return err
} else {
for key, data := range cmd.SecureJsonData {
encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey)
if err != nil {
return err
}
pluginSetting.SecureJsonData[key] = encryptedData
}
// add state change event on commit success
if pluginSetting.Enabled != cmd.Enabled {
sess.events = append(sess.events, &m.PluginStateChangedEvent{
PluginId: cmd.PluginId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
})
}
pluginSetting.Updated = time.Now()
pluginSetting.Enabled = cmd.Enabled
pluginSetting.JsonData = cmd.JsonData
pluginSetting.Pinned = cmd.Pinned
pluginSetting.PluginVersion = cmd.PluginVersion
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
return err
}
for key, data := range cmd.SecureJsonData {
encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey)
if err != nil {
return err
}
pluginSetting.SecureJsonData[key] = encryptedData
}
// add state change event on commit success
if pluginSetting.Enabled != cmd.Enabled {
sess.events = append(sess.events, &m.PluginStateChangedEvent{
PluginId: cmd.PluginId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
})
}
pluginSetting.Updated = time.Now()
pluginSetting.Enabled = cmd.Enabled
pluginSetting.JsonData = cmd.JsonData
pluginSetting.Pinned = cmd.Pinned
pluginSetting.PluginVersion = cmd.PluginVersion
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
return err
})
}
+10 -8
View File
@@ -72,6 +72,9 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
var prefs m.Preferences
exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs)
if err != nil {
return err
}
if !exists {
prefs = m.Preferences{
@@ -85,14 +88,13 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
}
_, err = sess.Insert(&prefs)
return err
} else {
prefs.HomeDashboardId = cmd.HomeDashboardId
prefs.Timezone = cmd.Timezone
prefs.Theme = cmd.Theme
prefs.Updated = time.Now()
prefs.Version += 1
_, err := sess.Id(prefs.Id).AllCols().Update(&prefs)
return err
}
prefs.HomeDashboardId = cmd.HomeDashboardId
prefs.Timezone = cmd.Timezone
prefs.Theme = cmd.Theme
prefs.Updated = time.Now()
prefs.Version += 1
_, err = sess.Id(prefs.Id).AllCols().Update(&prefs)
return err
})
}
+4 -4
View File
@@ -31,7 +31,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
has, err := x.Get(&quota)
if err != nil {
return err
} else if has == false {
} else if !has {
quota.Limit = query.Default
}
@@ -108,7 +108,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
return err
}
quota.Limit = cmd.Limit
if has == false {
if !has {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
@@ -133,7 +133,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
has, err := x.Get(&quota)
if err != nil {
return err
} else if has == false {
} else if !has {
quota.Limit = query.Default
}
@@ -210,7 +210,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
return err
}
quota.Limit = cmd.Limit
if has == false {
if !has {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
+34 -12
View File
@@ -77,7 +77,7 @@ func EnsureAdminUser() {
log.Info("Created default admin user: %v", setting.AdminUser)
}
func NewEngine() {
func NewEngine() *xorm.Engine {
x, err := getEngine()
if err != nil {
@@ -91,6 +91,8 @@ func NewEngine() {
sqlog.Error("Fail to initialize orm engine", "error", err)
os.Exit(1)
}
return x
}
func SetEngine(engine *xorm.Engine) (err error) {
@@ -121,7 +123,7 @@ func getEngine() (*xorm.Engine, error) {
}
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Pwd), protocol, DbCfg.Host, url.PathEscape(DbCfg.Name))
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
tlsCert, err := makeCert("custom", DbCfg)
@@ -140,13 +142,17 @@ func getEngine() (*xorm.Engine, error) {
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
port = fields[1]
}
if DbCfg.Pwd == "" {
DbCfg.Pwd = "''"
}
if DbCfg.User == "" {
DbCfg.User = "''"
}
cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode, DbCfg.ClientCertPath, DbCfg.ClientKeyPath, DbCfg.CaCertPath)
cnnstr = fmt.Sprintf("user='%s' password='%s' host='%s' port='%s' dbname='%s' sslmode='%s' sslcert='%s' sslkey='%s' sslrootcert='%s'",
strings.Replace(DbCfg.User, `'`, `\'`, -1),
strings.Replace(DbCfg.Pwd, `'`, `\'`, -1),
strings.Replace(host, `'`, `\'`, -1),
strings.Replace(port, `'`, `\'`, -1),
strings.Replace(DbCfg.Name, `'`, `\'`, -1),
strings.Replace(DbCfg.SslMode, `'`, `\'`, -1),
strings.Replace(DbCfg.ClientCertPath, `'`, `\'`, -1),
strings.Replace(DbCfg.ClientKeyPath, `'`, `\'`, -1),
strings.Replace(DbCfg.CaCertPath, `'`, `\'`, -1),
)
case "sqlite3":
if !filepath.IsAbs(DbCfg.Path) {
DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
@@ -166,7 +172,7 @@ func getEngine() (*xorm.Engine, error) {
engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime))
debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false)
debugSql := setting.Raw.Section("database").Key("log_queries").MustBool(false)
if !debugSql {
engine.SetLogger(&xorm.DiscardLogger{})
} else {
@@ -179,7 +185,7 @@ func getEngine() (*xorm.Engine, error) {
}
func LoadConfig() {
sec := setting.Cfg.Section("database")
sec := setting.Raw.Section("database")
cfgURL := sec.Key("url").String()
if len(cfgURL) != 0 {
@@ -258,7 +264,7 @@ func InitTestDB(t *testing.T) *xorm.Engine {
// x.ShowSQL()
if err != nil {
t.Fatalf("Failed to init in memory sqllite3 db %v", err)
t.Fatalf("Failed to init test database: %v", err)
}
sqlutil.CleanDB(x)
@@ -269,3 +275,19 @@ func InitTestDB(t *testing.T) *xorm.Engine {
return x
}
func IsTestDbMySql() bool {
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
return db == dbMySql
}
return false
}
func IsTestDbPostgres() bool {
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
return db == dbPostgres
}
return false
}
+2 -5
View File
@@ -13,16 +13,12 @@ func init() {
bus.AddHandler("sql", GetAdminStats)
}
var activeUserTimeLimit time.Duration = time.Hour * 24 * 30
var activeUserTimeLimit = time.Hour * 24 * 30
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
query.Result = make([]*m.DataSourceStats, 0)
err := x.SQL(rawSql).Find(&query.Result)
if err != nil {
return err
}
return err
}
@@ -68,6 +64,7 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
}
query.Result = &stats
return err
}
+1 -5
View File
@@ -210,11 +210,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
err := sess.Find(&query.Result)
if err != nil {
return err
}
return nil
return err
}
// AddTeamMember adds a user to a team
+2 -1
View File
@@ -74,6 +74,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
Convey("Should be able to return all teams a user is member of", func() {
groupId := group2.Result.Id
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]})
So(err, ShouldBeNil)
query := &m.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]}
err = GetTeamsByUser(query)
@@ -103,7 +104,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
So(err, ShouldBeNil)
err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId})
So(err, ShouldBeNil)
err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
So(err, ShouldBeNil)
+1 -1
View File
@@ -126,7 +126,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrTempUserNotFound
}
+22 -33
View File
@@ -47,10 +47,9 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
}
if has {
return org.Id, nil
} else {
org.Name = "Main Org."
org.Id = 1
}
org.Name = "Main Org."
org.Id = 1
} else {
org.Name = cmd.OrgName
if len(org.Name) == 0 {
@@ -154,7 +153,7 @@ func GetUserById(query *m.GetUserByIdQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@@ -168,18 +167,16 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
return m.ErrUserNotFound
}
user := new(m.User)
// Try and find the user by login first.
// It's not sufficient to assume that a LoginOrEmail with an "@" is an email.
user = &m.User{Login: query.LoginOrEmail}
user := &m.User{Login: query.LoginOrEmail}
has, err := x.Get(user)
if err != nil {
return err
}
if has == false && strings.Contains(query.LoginOrEmail, "@") {
if !has && strings.Contains(query.LoginOrEmail, "@") {
// If the user wasn't found, and it contains an "@" fallback to finding the
// user by email.
user = &m.User{Email: query.LoginOrEmail}
@@ -188,7 +185,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@@ -202,14 +199,12 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
return m.ErrUserNotFound
}
user := new(m.User)
user = &m.User{Email: query.Email}
user := &m.User{Email: query.Email}
has, err := x.Get(user)
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@@ -253,11 +248,8 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
Updated: time.Now(),
}
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
return err
}
return nil
_, err := sess.Id(cmd.UserId).Update(&user)
return err
})
}
@@ -271,11 +263,8 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
LastSeenAt: time.Now(),
}
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
return err
}
return nil
_, err := sess.Id(cmd.UserId).Update(&user)
return err
})
}
@@ -295,11 +284,12 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
}
return inTransaction(func(sess *DBSession) error {
user := m.User{}
sess.Id(cmd.UserId).Get(&user)
user := m.User{
Id: cmd.UserId,
OrgId: cmd.OrgId,
}
user.OrgId = cmd.OrgId
_, err := sess.Id(user.Id).Update(&user)
_, err := sess.Id(cmd.UserId).Update(&user)
return err
})
}
@@ -310,7 +300,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@@ -333,6 +323,7 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
sess.Join("INNER", "org", "org_user.org_id=org.id")
sess.Where("org_user.user_id=?", query.UserId)
sess.Cols("org.name", "org_user.role", "org_user.org_id")
sess.OrderBy("org.name")
err := sess.Find(&query.Result)
return err
}
@@ -444,6 +435,7 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
"DELETE FROM dashboard_acl WHERE user_id = ?",
"DELETE FROM preferences WHERE user_id = ?",
"DELETE FROM team_member WHERE user_id = ?",
"DELETE FROM user_auth WHERE user_id = ?",
}
for _, sql := range deletes {
@@ -478,10 +470,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
Updated: time.Now(),
}
if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
return err
}
return nil
_, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
return err
})
}
+148
View File
@@ -0,0 +1,148 @@
package sqlstore
import (
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddHandler("sql", GetUserByAuthInfo)
bus.AddHandler("sql", GetAuthInfo)
bus.AddHandler("sql", SetAuthInfo)
bus.AddHandler("sql", DeleteAuthInfo)
}
func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
user := &m.User{}
has := false
var err error
authQuery := &m.GetAuthInfoQuery{}
// Try to find the user by auth module and id first
if query.AuthModule != "" && query.AuthId != "" {
authQuery.AuthModule = query.AuthModule
authQuery.AuthId = query.AuthId
err = GetAuthInfo(authQuery)
if err != m.ErrUserNotFound {
if err != nil {
return err
}
// if user id was specified and doesn't match the user_auth entry, remove it
if query.UserId != 0 && query.UserId != authQuery.Result.UserId {
err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
UserAuth: authQuery.Result,
})
if err != nil {
sqlog.Error("Error removing user_auth entry", "error", err)
}
authQuery.Result = nil
} else {
has, err = x.Id(authQuery.Result.UserId).Get(user)
if err != nil {
return err
}
if !has {
// if the user has been deleted then remove the entry
err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
UserAuth: authQuery.Result,
})
if err != nil {
sqlog.Error("Error removing user_auth entry", "error", err)
}
authQuery.Result = nil
}
}
}
}
// If not found, try to find the user by id
if !has && query.UserId != 0 {
has, err = x.Id(query.UserId).Get(user)
if err != nil {
return err
}
}
// If not found, try to find the user by email address
if !has && query.Email != "" {
user = &m.User{Email: query.Email}
has, err = x.Get(user)
if err != nil {
return err
}
}
// If not found, try to find the user by login
if !has && query.Login != "" {
user = &m.User{Login: query.Login}
has, err = x.Get(user)
if err != nil {
return err
}
}
// No user found
if !has {
return m.ErrUserNotFound
}
// create authInfo record to link accounts
if authQuery.Result == nil && query.AuthModule != "" && query.AuthId != "" {
cmd2 := &m.SetAuthInfoCommand{
UserId: user.Id,
AuthModule: query.AuthModule,
AuthId: query.AuthId,
}
if err := SetAuthInfo(cmd2); err != nil {
return err
}
}
query.Result = user
return nil
}
func GetAuthInfo(query *m.GetAuthInfoQuery) error {
userAuth := &m.UserAuth{
AuthModule: query.AuthModule,
AuthId: query.AuthId,
}
has, err := x.Get(userAuth)
if err != nil {
return err
}
if !has {
return m.ErrUserNotFound
}
query.Result = userAuth
return nil
}
func SetAuthInfo(cmd *m.SetAuthInfoCommand) error {
return inTransaction(func(sess *DBSession) error {
authUser := &m.UserAuth{
UserId: cmd.UserId,
AuthModule: cmd.AuthModule,
AuthId: cmd.AuthId,
Created: time.Now(),
}
_, err := sess.Insert(authUser)
return err
})
}
func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error {
return inTransaction(func(sess *DBSession) error {
_, err := sess.Delete(cmd.UserAuth)
return err
})
}
+131
View File
@@ -0,0 +1,131 @@
package sqlstore
import (
"fmt"
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestUserAuth(t *testing.T) {
InitTestDB(t)
Convey("Given 5 users", t, func() {
var err error
var cmd *m.CreateUserCommand
users := []m.User{}
for i := 0; i < 5; i++ {
cmd = &m.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
}
err = CreateUser(cmd)
So(err, ShouldBeNil)
users = append(users, cmd.Result)
}
Reset(func() {
_, err := x.Exec("DELETE FROM org_user WHERE 1=1")
So(err, ShouldBeNil)
_, err = x.Exec("DELETE FROM org WHERE 1=1")
So(err, ShouldBeNil)
_, err = x.Exec("DELETE FROM " + dialect.Quote("user") + " WHERE 1=1")
So(err, ShouldBeNil)
_, err = x.Exec("DELETE FROM user_auth WHERE 1=1")
So(err, ShouldBeNil)
})
Convey("Can find existing user", func() {
// By Login
login := "loginuser0"
query := &m.GetUserByAuthInfoQuery{Login: login}
err = GetUserByAuthInfo(query)
So(err, ShouldBeNil)
So(query.Result.Login, ShouldEqual, login)
// By ID
id := query.Result.Id
query = &m.GetUserByAuthInfoQuery{UserId: id}
err = GetUserByAuthInfo(query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, id)
// By Email
email := "user1@test.com"
query = &m.GetUserByAuthInfoQuery{Email: email}
err = GetUserByAuthInfo(query)
So(err, ShouldBeNil)
So(query.Result.Email, ShouldEqual, email)
// Don't find nonexistent user
email = "nonexistent@test.com"
query = &m.GetUserByAuthInfoQuery{Email: email}
err = GetUserByAuthInfo(query)
So(err, ShouldEqual, m.ErrUserNotFound)
So(query.Result, ShouldBeNil)
})
Convey("Can set & locate by AuthModule and AuthId", func() {
// get nonexistent user_auth entry
query := &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
err = GetUserByAuthInfo(query)
So(err, ShouldEqual, m.ErrUserNotFound)
So(query.Result, ShouldBeNil)
// create user_auth entry
login := "loginuser0"
query.Login = login
err = GetUserByAuthInfo(query)
So(err, ShouldBeNil)
So(query.Result.Login, ShouldEqual, login)
// get via user_auth
query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
err = GetUserByAuthInfo(query)
So(err, ShouldBeNil)
So(query.Result.Login, ShouldEqual, login)
// get with non-matching id
id := query.Result.Id
query.UserId = id + 1
err = GetUserByAuthInfo(query)
So(err, ShouldBeNil)
So(query.Result.Login, ShouldEqual, "loginuser1")
// get via user_auth
query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
err = GetUserByAuthInfo(query)
So(err, ShouldBeNil)
So(query.Result.Login, ShouldEqual, "loginuser1")
// remove user
_, err = x.Exec("DELETE FROM "+dialect.Quote("user")+" WHERE id=?", query.Result.Id)
So(err, ShouldBeNil)
// get via user_auth for deleted user
query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
err = GetUserByAuthInfo(query)
So(err, ShouldEqual, m.ErrUserNotFound)
So(query.Result, ShouldBeNil)
})
})
}