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
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func AdminGetSettings(c *m.ReqContext) {
settings := make(map[string]interface{})
for _, section := range setting.Cfg.Sections() {
for _, section := range setting.Raw.Sections() {
jsonSec := make(map[string]interface{})
settings[section.Name()] = jsonSec
+9 -32
View File
@@ -2,7 +2,6 @@ package api
import (
"strings"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -15,9 +14,10 @@ import (
func GetAnnotations(c *m.ReqContext) Response {
query := &annotations.ItemQuery{
From: c.QueryInt64("from") / 1000,
To: c.QueryInt64("to") / 1000,
From: c.QueryInt64("from"),
To: c.QueryInt64("to"),
OrgId: c.OrgId,
UserId: c.QueryInt64("userId"),
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
@@ -37,7 +37,7 @@ func GetAnnotations(c *m.ReqContext) Response {
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
item.Time = item.Time * 1000
item.Time = item.Time
}
return JSON(200, items)
@@ -68,16 +68,12 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
UserId: c.UserId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
Epoch: cmd.Time / 1000,
Epoch: cmd.Time,
Text: cmd.Text,
Data: cmd.Data,
Tags: cmd.Tags,
}
if item.Epoch == 0 {
item.Epoch = time.Now().Unix()
}
if err := repo.Save(&item); err != nil {
return Error(500, "Failed to save annotation", err)
}
@@ -97,7 +93,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
}
item.Id = 0
item.Epoch = cmd.TimeEnd / 1000
item.Epoch = cmd.TimeEnd
if err := repo.Save(&item); err != nil {
return Error(500, "Failed save annotation for region end time", err)
@@ -132,9 +128,6 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
return Error(500, "Failed to save Graphite annotation", err)
}
if cmd.When == 0 {
cmd.When = time.Now().Unix()
}
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
@@ -163,7 +156,7 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Epoch: cmd.When,
Epoch: cmd.When * 1000,
Text: text,
Tags: tagsArray,
}
@@ -191,7 +184,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
OrgId: c.OrgId,
UserId: c.UserId,
Id: annotationID,
Epoch: cmd.Time / 1000,
Epoch: cmd.Time,
Text: cmd.Text,
Tags: cmd.Tags,
}
@@ -203,7 +196,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
if cmd.IsRegion {
itemRight := item
itemRight.RegionId = item.Id
itemRight.Epoch = cmd.TimeEnd / 1000
itemRight.Epoch = cmd.TimeEnd
// We don't know id of region right event, so set it to 0 and find then using query like
// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...
@@ -301,19 +294,3 @@ func canSave(c *m.ReqContext, repo annotations.Repository, annotationID int64) R
return nil
}
func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID int64) Response {
items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId})
if err != nil || len(items) == 0 {
return Error(500, "Could not find annotation to update", err)
}
dashboardID := items[0].DashboardId
if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
return nil
}
+12 -4
View File
@@ -23,7 +23,7 @@ func (hs *HTTPServer) registerRoutes() {
// automatically set HEAD for every GET
macaronR.SetAutoHead(true)
r := newRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)
r := hs.RouteRegister
// not logged in views
r.Get("/", reqSignedIn, Index)
@@ -149,8 +149,6 @@ func (hs *HTTPServer) registerRoutes() {
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
teamsRoute.Get("/search", wrap(SearchTeams))
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
@@ -159,6 +157,12 @@ func (hs *HTTPServer) registerRoutes() {
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
}, reqOrgAdmin)
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
teamsRoute.Get("/search", wrap(SearchTeams))
})
// org information available to all users.
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent))
@@ -170,7 +174,6 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
@@ -184,6 +187,11 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
}, reqOrgAdmin)
// current org without requirement of user to be org admin
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
})
// create new org
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
+3 -6
View File
@@ -226,7 +226,7 @@ func (this *thunderTask) Fetch() {
this.Done()
}
var client *http.Client = &http.Client{
var client = &http.Client{
Timeout: time.Second * 2,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
@@ -258,9 +258,6 @@ func (this *thunderTask) fetch() error {
this.Avatar.data = &bytes.Buffer{}
writer := bufio.NewWriter(this.Avatar.data)
if _, err = io.Copy(writer, resp.Body); err != nil {
return err
}
return nil
_, err = io.Copy(writer, resp.Body)
return err
}
+12 -1
View File
@@ -102,6 +102,16 @@ func GetDashboard(c *m.ReqContext) Response {
meta.FolderUrl = query.Result.GetUrl()
}
isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id}
err = bus.Dispatch(isDashboardProvisioned)
if err != nil {
return Error(500, "Error while checking if dashboard is provisioned", err)
}
if isDashboardProvisioned.Result {
meta.Provisioned = true
}
// make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version)
@@ -228,7 +238,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
err == m.ErrDashboardWithSameUIDExists ||
err == m.ErrFolderNotFound ||
err == m.ErrDashboardFolderCannotHaveParent ||
err == m.ErrDashboardFolderNameExists {
err == m.ErrDashboardFolderNameExists ||
err == m.ErrDashboardCannotSaveProvisionedDashboard {
return Error(400, err.Error(), nil)
}
+5
View File
@@ -29,6 +29,11 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
}
for _, perm := range acl {
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
if perm.TeamId > 0 {
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
}
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
+1 -1
View File
@@ -143,7 +143,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) {
})
})
Convey("When trying to override inherited permissions with lower presedence", func() {
Convey("When trying to override inherited permissions with lower precedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
+21
View File
@@ -42,6 +42,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
@@ -192,6 +197,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
fakeDash.HasAcl = true
setting.ViewersCanEdit = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{fakeDash}
query.Result = dashboards
@@ -625,6 +635,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
dashTwo.FolderId = 3
dashTwo.HasAcl = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{dashOne, dashTwo}
query.Result = dashboards
@@ -720,6 +735,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardCannotSaveProvisionedDashboard, ExpectedStatusCode: 400},
{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
}
@@ -750,6 +766,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{
Data: simplejson.NewFromAny(map[string]interface{}{
+1
View File
@@ -28,6 +28,7 @@ type DashboardMeta struct {
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
}
type DashboardFullWithMeta struct {
+16 -15
View File
@@ -22,21 +22,22 @@ type LoginCommand struct {
}
type CurrentUser struct {
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
LightTheme bool `json:"lightTheme"`
OrgCount int `json:"orgCount"`
OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"`
Timezone string `json:"timezone"`
Locale string `json:"locale"`
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
LightTheme bool `json:"lightTheme"`
OrgCount int `json:"orgCount"`
OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"`
Timezone string `json:"timezone"`
Locale string `json:"locale"`
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"`
}
type MetricRequest struct {
+6
View File
@@ -33,6 +33,12 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
perm.FolderId = folder.Id
perm.DashboardId = 0
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
if perm.TeamId > 0 {
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
}
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
+34 -15
View File
@@ -26,27 +26,34 @@ import (
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
func init() {
registry.RegisterService(&HTTPServer{})
}
type HTTPServer struct {
log log.Logger
macaron *macaron.Macaron
context context.Context
streamManager *live.StreamManager
cache *gocache.Cache
httpSrv *http.Server
httpSrv *http.Server
RouteRegister RouteRegister `inject:""`
Bus bus.Bus `inject:""`
}
func NewHTTPServer() *HTTPServer {
return &HTTPServer{
log: log.New("http.server"),
cache: gocache.New(5*time.Minute, 10*time.Minute),
}
func (hs *HTTPServer) Init() error {
hs.log = log.New("http.server")
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
return nil
}
func (hs *HTTPServer) Start(ctx context.Context) error {
func (hs *HTTPServer) Run(ctx context.Context) error {
var err error
hs.context = ctx
@@ -57,9 +64,20 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
hs.streamManager.Run(ctx)
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
// handle http shutdown on server context done
go func() {
<-ctx.Done()
// Hacky fix for race condition between ListenAndServe and Shutdown
time.Sleep(time.Millisecond * 100)
if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
hs.log.Error("Failed to shutdown server", "error", err)
}
}()
switch setting.Protocol {
case setting.HTTP:
err = hs.httpSrv.ListenAndServe()
@@ -96,12 +114,6 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
return err
}
func (hs *HTTPServer) Shutdown(ctx context.Context) error {
err := hs.httpSrv.Shutdown(ctx)
hs.log.Info("Stopped HTTP server")
return err
}
func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
if certfile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
@@ -139,7 +151,7 @@ func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
}
hs.httpSrv.TLSConfig = tlsCfg
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
}
@@ -162,6 +174,7 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
hs.mapStatic(m, route.Directory, "", pluginRoute)
}
hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
hs.mapStatic(m, setting.StaticRootPath, "", "public")
hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
@@ -229,6 +242,12 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
}
if prefix == "public/build" {
headers = func(c *macaron.Context) {
c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
}
}
if setting.Env == setting.DEV {
headers = func(c *macaron.Context) {
c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
+41 -16
View File
@@ -42,23 +42,29 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
settings["appSubUrl"] = ""
}
hasEditPermissionInFoldersQuery := m.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := bus.Dispatch(&hasEditPermissionInFoldersQuery); err != nil {
return nil, err
}
var data = dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: c.UserId,
IsSignedIn: c.IsSignedIn,
Login: c.Login,
Email: c.Email,
Name: c.Name,
OrgCount: c.OrgCount,
OrgId: c.OrgId,
OrgName: c.OrgName,
OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email),
IsGrafanaAdmin: c.IsGrafanaAdmin,
LightTheme: prefs.Theme == "light",
Timezone: prefs.Timezone,
Locale: locale,
HelpFlags1: c.HelpFlags1,
Id: c.UserId,
IsSignedIn: c.IsSignedIn,
Login: c.Login,
Email: c.Email,
Name: c.Name,
OrgCount: c.OrgCount,
OrgId: c.OrgId,
OrgName: c.OrgName,
OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email),
IsGrafanaAdmin: c.IsGrafanaAdmin,
LightTheme: prefs.Theme == "light",
Timezone: prefs.Timezone,
Locale: locale,
HelpFlags1: c.HelpFlags1,
HasEditPermissionInFolders: hasEditPermissionInFoldersQuery.Result,
},
Settings: settings,
Theme: prefs.Theme,
@@ -117,10 +123,28 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
Children: dashboardChildNavs,
})
if setting.ExploreEnabled {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Explore",
Id: "explore",
SubTitle: "Explore your data",
Icon: "fa fa-rocket",
Url: setting.AppSubUrl + "/explore",
Children: []*dtos.NavLink{
{Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore"},
},
})
}
if c.IsSignedIn {
// Only set login if it's different from the name
var login string
if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
login = c.SignedInUser.Login
}
profileNode := &dtos.NavLink{
Text: c.SignedInUser.NameOrFallback(),
SubTitle: c.SignedInUser.Login,
SubTitle: login,
Id: "profile",
Img: data.User.GravatarUrl,
Url: setting.AppSubUrl + "/profile",
@@ -284,6 +308,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
Id: "help",
Url: "#",
Icon: "gicon gicon-question",
+6 -5
View File
@@ -101,13 +101,14 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
return Error(401, "Login is disabled", nil)
}
authQuery := login.LoginUserQuery{
Username: cmd.User,
Password: cmd.Password,
IpAddress: c.Req.RemoteAddr,
authQuery := &m.LoginUserQuery{
ReqContext: c,
Username: cmd.User,
Password: cmd.Password,
IpAddress: c.Req.RemoteAddr,
}
if err := bus.Dispatch(&authQuery); err != nil {
if err := bus.Dispatch(authQuery); err != nil {
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
return Error(401, "Invalid username or password", err)
}
+27 -46
View File
@@ -6,7 +6,6 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net/http"
@@ -16,22 +15,15 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
)
var (
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
ErrUsersQuotaReached = errors.New("Users quota reached")
ErrNoEmail = errors.New("Login provider didn't return an email address")
oauthLogger = log.New("oauth")
)
var oauthLogger = log.New("oauth")
func GenStateString() string {
rnd := make([]byte, 32)
@@ -56,7 +48,7 @@ func OAuthLogin(ctx *m.ReqContext) {
if errorParam != "" {
errorDesc := ctx.Query("error_description")
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
return
}
@@ -149,54 +141,43 @@ func OAuthLogin(ctx *m.ReqContext) {
// validate that we got at least an email address
if userInfo.Email == "" {
redirectWithError(ctx, ErrNoEmail)
redirectWithError(ctx, login.ErrNoEmail)
return
}
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
redirectWithError(ctx, ErrEmailNotAllowed)
redirectWithError(ctx, login.ErrEmailNotAllowed)
return
}
userQuery := m.GetUserByEmailQuery{Email: userInfo.Email}
err = bus.Dispatch(&userQuery)
extUser := &m.ExternalUserInfo{
AuthModule: "oauth_" + name,
AuthId: userInfo.Id,
Name: userInfo.Name,
Login: userInfo.Login,
Email: userInfo.Email,
OrgRoles: map[int64]m.RoleType{},
}
// create account if missing
if err == m.ErrUserNotFound {
if !connect.IsSignupAllowed() {
redirectWithError(ctx, ErrSignUpNotAllowed)
return
}
limitReached, err := quota.QuotaReached(ctx, "user")
if err != nil {
ctx.Handle(500, "Failed to get user quota", err)
return
}
if limitReached {
redirectWithError(ctx, ErrUsersQuotaReached)
return
}
cmd := m.CreateUserCommand{
Login: userInfo.Login,
Email: userInfo.Email,
Name: userInfo.Name,
Company: userInfo.Company,
DefaultOrgRole: userInfo.Role,
}
if userInfo.Role != "" {
extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
}
if err = bus.Dispatch(&cmd); err != nil {
ctx.Handle(500, "Failed to create account", err)
return
}
userQuery.Result = &cmd.Result
} else if err != nil {
ctx.Handle(500, "Unexpected error", err)
// add/update user in grafana
cmd := &m.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: connect.IsSignupAllowed(),
}
err = bus.Dispatch(cmd)
if err != nil {
redirectWithError(ctx, err)
return
}
// login
loginUserWithUser(userQuery.Result, ctx)
loginUserWithUser(cmd.Result, ctx)
metrics.M_Api_Login_OAuth.Inc()
+1 -1
View File
@@ -75,7 +75,7 @@ func GetTestDataScenarios(c *m.ReqContext) Response {
return JSON(200, &result)
}
// Genereates a index out of range error
// Generates a index out of range error
func GenerateError(c *m.ReqContext) Response {
var array []string
return JSON(200, array[20])
+1 -1
View File
@@ -33,7 +33,7 @@ func ValidateOrgPlaylist(c *m.ReqContext) {
return
}
if len(items) == 0 {
if len(items) == 0 && c.Context.Req.Method != "DELETE" {
c.JsonApiErr(404, "Playlist is empty", itemsErr)
return
}
-6
View File
@@ -189,12 +189,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
}
func (proxy *DataSourceProxy) validateRequest() error {
if proxy.ds.Type == m.DS_INFLUXDB {
if proxy.ctx.Query("db") != proxy.ds.Database {
return errors.New("Datasource is not configured to allow this database")
}
}
if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
return errors.New("Target url is not a valid target")
}
+4 -1
View File
@@ -11,6 +11,8 @@ type Router interface {
Get(pattern string, handlers ...macaron.Handler) *macaron.Route
}
// RouteRegister allows you to add routes and macaron.Handlers
// that the web server should serve.
type RouteRegister interface {
Get(string, ...macaron.Handler)
Post(string, ...macaron.Handler)
@@ -26,7 +28,8 @@ type RouteRegister interface {
type RegisterNamedMiddleware func(name string) macaron.Handler
func newRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
func NewRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
return &routeRegister{
prefix: "",
routes: []route{},
+3 -3
View File
@@ -51,7 +51,7 @@ func TestRouteSimpleRegister(t *testing.T) {
}
// Setup
rr := newRouteRegister(func(name string) macaron.Handler {
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
@@ -96,7 +96,7 @@ func TestRouteGroupedRegister(t *testing.T) {
}
// Setup
rr := newRouteRegister()
rr := NewRouteRegister()
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
@@ -150,7 +150,7 @@ func TestNamedMiddlewareRouteRegister(t *testing.T) {
}
// Setup
rr := newRouteRegister(func(name string) macaron.Handler {
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
+1 -1
View File
@@ -48,7 +48,7 @@ type StaticOptions struct {
// Expires defines which user-defined function to use for producing a HTTP Expires Header
// https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
AddHeaders func(ctx *macaron.Context)
// FileSystem is the interface for supporting any implmentation of file system.
// FileSystem is the interface for supporting any implementation of file system.
FileSystem http.FileSystem
}
+12 -7
View File
@@ -2,7 +2,7 @@ package bus
import (
"context"
"fmt"
"errors"
"reflect"
)
@@ -10,6 +10,8 @@ type HandlerFunc interface{}
type CtxHandlerFunc func()
type Msg interface{}
var ErrHandlerNotFound = errors.New("handler not found")
type Bus interface {
Dispatch(msg Msg) error
DispatchCtx(ctx context.Context, msg Msg) error
@@ -38,12 +40,17 @@ func New() Bus {
return bus
}
// Want to get rid of global bus
func GetBus() Bus {
return globalBus
}
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var handler = b.handlers[msgName]
if handler == nil {
return fmt.Errorf("handler not found for %s", msgName)
return ErrHandlerNotFound
}
var params = make([]reflect.Value, 2)
@@ -54,9 +61,8 @@ func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
err := ret[0].Interface()
if err == nil {
return nil
} else {
return err.(error)
}
return err.(error)
}
func (b *InProcBus) Dispatch(msg Msg) error {
@@ -64,7 +70,7 @@ func (b *InProcBus) Dispatch(msg Msg) error {
var handler = b.handlers[msgName]
if handler == nil {
return fmt.Errorf("handler not found for %s", msgName)
return ErrHandlerNotFound
}
var params = make([]reflect.Value, 1)
@@ -74,9 +80,8 @@ func (b *InProcBus) Dispatch(msg Msg) error {
err := ret[0].Interface()
if err == nil {
return nil
} else {
return err.(error)
}
return err.(error)
}
func (b *InProcBus) Publish(msg Msg) error {
+2 -1
View File
@@ -15,7 +15,8 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
return func(context *cli.Context) {
cmd := &contextCommandLine{context}
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
Config: cmd.String("config"),
HomePath: cmd.String("homepath"),
Args: flag.Args(),
@@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
fileInfo, err := os.Stat(pluginsDir)
if err != nil {
if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
return errors.New(fmt.Sprintf("pluginsDir (%s) is not a directory", pluginsDir))
return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
}
return nil
}
+1 -1
View File
@@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
return fmt.Errorf("error: %s", err)
}
if pluginDirInfo.IsDir() == false {
if !pluginDirInfo.IsDir() {
return errors.New("plugin path is not a directory")
}
@@ -3,12 +3,11 @@ package commands
import (
"errors"
"fmt"
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"strings"
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
)
var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
func removeCommand(c CommandLine) error {
@@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
for _, p := range pluginsToUpgrade {
logger.Infof("Updating %v \n", p.Id)
var err error
err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
err := s.RemoveInstalledPlugin(pluginsDir, p.Id)
if err != nil {
return err
}
+4 -1
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"path"
"runtime"
"time"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
@@ -42,7 +43,7 @@ func Init(version string, skipTLSVerify bool) {
}
HttpClient = http.Client{
Timeout: time.Duration(10 * time.Second),
Timeout: 10 * time.Second,
Transport: tr,
}
}
@@ -155,6 +156,8 @@ func sendRequest(repoUrl string, subPaths ...string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
req.Header.Set("grafana-version", grafanaVersion)
req.Header.Set("grafana-os", runtime.GOOS)
req.Header.Set("grafana-arch", runtime.GOARCH)
req.Header.Set("User-Agent", "grafana "+grafanaVersion)
if err != nil {
+11 -25
View File
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/setting"
_ "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
@@ -33,12 +34,11 @@ import (
var version = "5.0.0"
var commit = "NA"
var buildstamp string
var build_date string
var enterprise string
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
var pidFile = flag.String("pidfile", "", "path to pid file")
var exitChan = make(chan int)
func main() {
v := flag.Bool("v", false, "prints current version and exits")
@@ -77,45 +77,31 @@ func main() {
setting.BuildVersion = version
setting.BuildCommit = commit
setting.BuildStamp = buildstampInt64
setting.Enterprise, _ = strconv.ParseBool(enterprise)
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
shutdownCompleted := make(chan int)
server := NewGrafanaServer()
go listenToSystemSignals(server, shutdownCompleted)
go listenToSystemSignals(server)
go func() {
code := 0
if err := server.Start(); err != nil {
log.Error2("Startup failed", "error", err)
code = 1
}
err := server.Run()
exitChan <- code
}()
code := <-shutdownCompleted
log.Info2("Grafana shutdown completed.", "code", code)
trace.Stop()
log.Close()
os.Exit(code)
server.Exit(err)
}
func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
func listenToSystemSignals(server *GrafanaServerImpl) {
signalChan := make(chan os.Signal, 1)
ignoreChan := make(chan os.Signal, 1)
code := 0
signal.Notify(ignoreChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
select {
case sig := <-signalChan:
trace.Stop() // Stops trace if profiling has been enabled
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
shutdownCompleted <- 0
case code = <-exitChan:
trace.Stop() // Stops trace if profiling has been enabled
server.Shutdown(code, "startup error")
shutdownCompleted <- code
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
}
}
+121 -70
View File
@@ -8,27 +8,36 @@ import (
"net"
"os"
"path/filepath"
"reflect"
"strconv"
"time"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/facebookgo/inject"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/dashboards"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
"github.com/grafana/grafana/pkg/tracing"
// self registering services
_ "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/metrics"
_ "github.com/grafana/grafana/pkg/plugins"
_ "github.com/grafana/grafana/pkg/services/alerting"
_ "github.com/grafana/grafana/pkg/services/cleanup"
_ "github.com/grafana/grafana/pkg/services/notifications"
_ "github.com/grafana/grafana/pkg/services/provisioning"
_ "github.com/grafana/grafana/pkg/services/search"
)
func NewGrafanaServer() *GrafanaServerImpl {
@@ -40,110 +49,152 @@ func NewGrafanaServer() *GrafanaServerImpl {
shutdownFn: shutdownFn,
childRoutines: childRoutines,
log: log.New("server"),
cfg: setting.NewCfg(),
}
}
type GrafanaServerImpl struct {
context context.Context
shutdownFn context.CancelFunc
childRoutines *errgroup.Group
log log.Logger
context context.Context
shutdownFn context.CancelFunc
childRoutines *errgroup.Group
log log.Logger
cfg *setting.Cfg
shutdownReason string
shutdownInProgress bool
httpServer *api.HTTPServer
RouteRegister api.RouteRegister `inject:""`
HttpServer *api.HTTPServer `inject:""`
}
func (g *GrafanaServerImpl) Start() error {
g.initLogging()
func (g *GrafanaServerImpl) Run() error {
g.loadConfiguration()
g.writePIDFile()
initSql()
// initSql
sqlstore.NewEngine() // TODO: this should return an error
sqlstore.EnsureAdminUser()
metrics.Init(setting.Cfg)
search.Init()
login.Init()
social.NewOAuthService()
pluginManager, err := plugins.NewPluginManager(g.context)
if err != nil {
return fmt.Errorf("Failed to start plugins. error: %v", err)
}
g.childRoutines.Go(func() error { return pluginManager.Run(g.context) })
if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil {
return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
}
tracingCloser, err := tracing.Init(setting.Cfg)
tracingCloser, err := tracing.Init(g.cfg.Raw)
if err != nil {
return fmt.Errorf("Tracing settings is not valid. error: %v", err)
}
defer tracingCloser.Close()
// init alerting
if setting.AlertingEnabled && setting.ExecuteAlerts {
engine := alerting.NewEngine()
g.childRoutines.Go(func() error { return engine.Run(g.context) })
serviceGraph := inject.Graph{}
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
serviceGraph.Provide(&inject.Object{Value: g.cfg})
serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()})
serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
// self registered services
services := registry.GetServices()
// Add all services to dependency graph
for _, service := range services {
serviceGraph.Provide(&inject.Object{Value: service})
}
// cleanup service
cleanUpService := cleanup.NewCleanUpService()
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
serviceGraph.Provide(&inject.Object{Value: g})
if err = notifications.Init(); err != nil {
return fmt.Errorf("Notification service failed to initialize. error: %v", err)
// Inject dependencies to services
if err := serviceGraph.Populate(); err != nil {
return fmt.Errorf("Failed to populate service dependency: %v", err)
}
// Init & start services
for _, service := range services {
if registry.IsDisabled(service) {
continue
}
g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name())
if err := service.Init(); err != nil {
return fmt.Errorf("Service init failed: %v", err)
}
}
// Start background services
for index := range services {
service, ok := services[index].(registry.BackgroundService)
if !ok {
continue
}
if registry.IsDisabled(services[index]) {
continue
}
g.childRoutines.Go(func() error {
// Skip starting new service when shutting down
// Can happen when service stop/return during startup
if g.shutdownInProgress {
return nil
}
err := service.Run(g.context)
// If error is not canceled then the service crashed
if err != context.Canceled && err != nil {
g.log.Error("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err)
} else {
g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err)
}
// Mark that we are in shutdown mode
// So more services are not started
g.shutdownInProgress = true
return err
})
}
sendSystemdNotification("READY=1")
return g.startHttpServer()
return g.childRoutines.Wait()
}
func initSql() {
sqlstore.NewEngine()
sqlstore.EnsureAdminUser()
}
func (g *GrafanaServerImpl) initLogging() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
func (g *GrafanaServerImpl) loadConfiguration() {
err := g.cfg.Load(&setting.CommandLineArgs{
Config: *configFile,
HomePath: *homePath,
Args: flag.Args(),
})
if err != nil {
g.log.Error(err.Error())
fmt.Fprintf(os.Stderr, "Failed to start grafana. error: %s\n", err.Error())
os.Exit(1)
}
g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
setting.LogConfigurationInfo()
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
g.cfg.LogConfigSources()
}
func (g *GrafanaServerImpl) startHttpServer() error {
g.httpServer = api.NewHTTPServer()
err := g.httpServer.Start(g.context)
if err != nil {
return fmt.Errorf("Fail to start server. error: %v", err)
}
return nil
}
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
g.log.Info("Shutdown started", "code", code, "reason", reason)
err := g.httpServer.Shutdown(g.context)
if err != nil {
g.log.Error("Failed to shutdown server", "error", err)
}
func (g *GrafanaServerImpl) Shutdown(reason string) {
g.log.Info("Shutdown started", "reason", reason)
g.shutdownReason = reason
g.shutdownInProgress = true
// call cancel func on root context
g.shutdownFn()
err = g.childRoutines.Wait()
if err != nil && err != context.Canceled {
g.log.Error("Server shutdown completed with an error", "error", err)
// wait for child routines
g.childRoutines.Wait()
}
func (g *GrafanaServerImpl) Exit(reason error) {
// default exit code is 1
code := 1
if reason == context.Canceled && g.shutdownReason != "" {
reason = fmt.Errorf(g.shutdownReason)
code = 0
}
g.log.Error("Server shutdown", "reason", reason)
os.Exit(code)
}
func (g *GrafanaServerImpl) writePIDFile() {
+2 -2
View File
@@ -33,7 +33,7 @@ func New(orgId int64, name string) KeyGenResult {
jsonString, _ := json.Marshal(jsonKey)
result.ClientSecret = base64.StdEncoding.EncodeToString([]byte(jsonString))
result.ClientSecret = base64.StdEncoding.EncodeToString(jsonString)
return result
}
@@ -44,7 +44,7 @@ func Decode(keyString string) (*ApiKeyJson, error) {
}
var keyObj ApiKeyJson
err = json.Unmarshal([]byte(jsonString), &keyObj)
err = json.Unmarshal(jsonString, &keyObj)
if err != nil {
return nil, ErrInvalidApiKey
}
+4 -4
View File
@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
diff "github.com/yudai/gojsondiff"
deltaFormatter "github.com/yudai/gojsondiff/formatter"
@@ -15,11 +14,8 @@ import (
var (
// ErrUnsupportedDiffType occurs when an invalid diff type is used.
ErrUnsupportedDiffType = errors.New("dashdiff: unsupported diff type")
// ErrNilDiff occurs when two compared interfaces are identical.
ErrNilDiff = errors.New("dashdiff: diff is nil")
diffLogger = log.New("dashdiffs")
)
type DiffType int
@@ -145,5 +141,9 @@ func getDiff(baseData, newData *simplejson.Json) (interface{}, diff.Diff, error)
left := make(map[string]interface{})
err = json.Unmarshal(leftBytes, &left)
if err != nil {
return nil, nil, err
}
return left, jsonDiff, nil
}
+1 -1
View File
@@ -22,7 +22,7 @@ const (
)
var (
// changeTypeToSymbol is used for populating the terminating characer in
// changeTypeToSymbol is used for populating the terminating character in
// the diff
changeTypeToSymbol = map[ChangeType]string{
ChangeNil: "",
+109 -184
View File
@@ -134,9 +134,8 @@ func (v *Value) get(key string) (*Value, error) {
child, ok := obj.Map()[key]
if ok {
return child, nil
} else {
return nil, KeyNotFoundError{key}
}
return nil, KeyNotFoundError{key}
}
return nil, err
@@ -174,17 +173,13 @@ func (v *Object) GetObject(keys ...string) (*Object, error) {
if err != nil {
return nil, err
} else {
obj, err := child.Object()
if err != nil {
return nil, err
} else {
return obj, nil
}
}
obj, err := child.Object()
if err != nil {
return nil, err
}
return obj, nil
}
// Gets the value at key path and attempts to typecast the value into a string.
@@ -196,18 +191,17 @@ func (v *Object) GetString(keys ...string) (string, error) {
if err != nil {
return "", err
} else {
return child.String()
}
return child.String()
}
func (v *Object) MustGetString(path string, def string) string {
keys := strings.Split(path, ".")
if str, err := v.GetString(keys...); err != nil {
str, err := v.GetString(keys...)
if err != nil {
return def
} else {
return str
}
return str
}
// Gets the value at key path and attempts to typecast the value into null.
@@ -233,16 +227,13 @@ func (v *Object) GetNumber(keys ...string) (json.Number, error) {
if err != nil {
return "", err
} else {
n, err := child.Number()
if err != nil {
return "", err
} else {
return n, nil
}
}
n, err := child.Number()
if err != nil {
return "", err
}
return n, nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@@ -254,16 +245,13 @@ func (v *Object) GetFloat64(keys ...string) (float64, error) {
if err != nil {
return 0, err
} else {
n, err := child.Float64()
if err != nil {
return 0, err
} else {
return n, nil
}
}
n, err := child.Float64()
if err != nil {
return 0, err
}
return n, nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@@ -275,16 +263,13 @@ func (v *Object) GetInt64(keys ...string) (int64, error) {
if err != nil {
return 0, err
} else {
n, err := child.Int64()
if err != nil {
return 0, err
} else {
return n, nil
}
}
n, err := child.Int64()
if err != nil {
return 0, err
}
return n, nil
}
// Gets the value at key path and attempts to typecast the value into a float64.
@@ -296,9 +281,8 @@ func (v *Object) GetInterface(keys ...string) (interface{}, error) {
if err != nil {
return nil, err
} else {
return child.Interface(), nil
}
return child.Interface(), nil
}
// Gets the value at key path and attempts to typecast the value into a bool.
@@ -311,7 +295,6 @@ func (v *Object) GetBoolean(keys ...string) (bool, error) {
if err != nil {
return false, err
}
return child.Boolean()
}
@@ -328,11 +311,8 @@ func (v *Object) GetValueArray(keys ...string) ([]*Value, error) {
if err != nil {
return nil, err
} else {
return child.Array()
}
return child.Array()
}
// Gets the value at key path and attempts to typecast the value into an array of objects.
@@ -347,30 +327,24 @@ func (v *Object) GetObjectArray(keys ...string) ([]*Object, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]*Object, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.
Object()
if err != nil {
return nil, err
} else {
typedArray := make([]*Object, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.
Object()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of string.
@@ -387,29 +361,23 @@ func (v *Object) GetStringArray(keys ...string) ([]string, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]string, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.String()
if err != nil {
return nil, err
} else {
typedArray := make([]string, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.String()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of numbers.
@@ -424,29 +392,23 @@ func (v *Object) GetNumberArray(keys ...string) ([]json.Number, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]json.Number, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Number()
if err != nil {
return nil, err
} else {
typedArray := make([]json.Number, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Number()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of floats.
@@ -456,29 +418,23 @@ func (v *Object) GetFloat64Array(keys ...string) ([]float64, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]float64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Float64()
if err != nil {
return nil, err
} else {
typedArray := make([]float64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Float64()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of ints.
@@ -488,29 +444,23 @@ func (v *Object) GetInt64Array(keys ...string) ([]int64, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]int64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Int64()
if err != nil {
return nil, err
} else {
typedArray := make([]int64, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Int64()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of bools.
@@ -520,29 +470,23 @@ func (v *Object) GetBooleanArray(keys ...string) ([]bool, error) {
if err != nil {
return nil, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return nil, err
}
typedArray := make([]bool, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Boolean()
if err != nil {
return nil, err
} else {
typedArray := make([]bool, len(array))
for index, arrayItem := range array {
typedArrayItem, err := arrayItem.Boolean()
if err != nil {
return nil, err
} else {
typedArray[index] = typedArrayItem
}
}
return typedArray, nil
}
typedArray[index] = typedArrayItem
}
return typedArray, nil
}
// Gets the value at key path and attempts to typecast the value into an array of nulls.
@@ -552,29 +496,23 @@ func (v *Object) GetNullArray(keys ...string) (int64, error) {
if err != nil {
return 0, err
} else {
}
array, err := child.Array()
array, err := child.Array()
if err != nil {
return 0, err
}
var length int64 = 0
for _, arrayItem := range array {
err := arrayItem.Null()
if err != nil {
return 0, err
} else {
var length int64 = 0
for _, arrayItem := range array {
err := arrayItem.Null()
if err != nil {
return 0, err
} else {
length++
}
}
return length, nil
}
length++
}
return length, nil
}
// Returns an error if the value is not actually null
@@ -585,15 +523,12 @@ func (v *Value) Null() error {
switch v.data.(type) {
case nil:
valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
break
}
if valid {
return nil
}
return ErrNotNull
}
// Attempts to typecast the current value into an array.
@@ -607,24 +542,19 @@ func (v *Value) Array() ([]*Value, error) {
switch v.data.(type) {
case []interface{}:
valid = true
break
}
// Unsure if this is a good way to use slices, it's probably not
var slice []*Value
if valid {
for _, element := range v.data.([]interface{}) {
child := Value{element, true}
slice = append(slice, &child)
}
return slice, nil
}
return slice, ErrNotArray
}
// Attempts to typecast the current value into a number.
@@ -638,7 +568,6 @@ func (v *Value) Number() (json.Number, error) {
switch v.data.(type) {
case json.Number:
valid = true
break
}
if valid {
@@ -687,7 +616,6 @@ func (v *Value) Boolean() (bool, error) {
switch v.data.(type) {
case bool:
valid = true
break
}
if valid {
@@ -709,7 +637,6 @@ func (v *Value) Object() (*Object, error) {
switch v.data.(type) {
case map[string]interface{}:
valid = true
break
}
if valid {
@@ -746,7 +673,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
switch v.data.(type) {
case []interface{}:
valid = true
break
}
// Unsure if this is a good way to use slices, it's probably not
@@ -782,7 +708,6 @@ func (v *Value) String() (string, error) {
switch v.data.(type) {
case string:
valid = true
break
}
if valid {
+10 -6
View File
@@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
}
func (assert *Assert) True(value bool, message string) {
if value == false {
if !value {
log.Panicln("Assert: ", message)
}
}
@@ -60,6 +60,7 @@ func TestFirst(t *testing.T) {
}`
j, err := NewObjectFromBytes([]byte(testJSON))
assert.True(err == nil, "failed to create new object from bytes")
a, err := j.GetObject("address")
assert.True(a != nil && err == nil, "failed to create json from string")
@@ -76,10 +77,10 @@ func TestFirst(t *testing.T) {
assert.True(s == "fallback", "must get string return fallback")
s, err = j.GetString("name")
assert.True(s == "anton" && err == nil, "name shoud match")
assert.True(s == "anton" && err == nil, "name should match")
s, err = j.GetString("address", "street")
assert.True(s == "Street 42" && err == nil, "street shoud match")
assert.True(s == "Street 42" && err == nil, "street should match")
//log.Println("s: ", s.String())
_, err = j.GetNumber("age")
@@ -108,6 +109,7 @@ func TestFirst(t *testing.T) {
//log.Println("address: ", address)
s, err = address.GetString("street")
assert.True(s == "Street 42" && err == nil, "street mismatching")
addressAsString, err := j.GetString("address")
assert.True(addressAsString == "" && err != nil, "address should not be an string")
@@ -119,13 +121,13 @@ func TestFirst(t *testing.T) {
assert.True(s == "" && err != nil, "nonexistent string fail")
b, err := j.GetBoolean("true")
assert.True(b == true && err == nil, "bool true test")
assert.True(b && err == nil, "bool true test")
b, err = j.GetBoolean("false")
assert.True(b == false && err == nil, "bool false test")
assert.True(!b && err == nil, "bool false test")
b, err = j.GetBoolean("invalid_field")
assert.True(b == false && err != nil, "bool invalid test")
assert.True(!b && err != nil, "bool invalid test")
list, err := j.GetValueArray("list")
assert.True(list != nil && err == nil, "list should be an array")
@@ -148,6 +150,7 @@ func TestFirst(t *testing.T) {
//assert.True(element.IsObject() == true, "first fail")
element, err := elementValue.Object()
assert.True(err == nil, "create element fail")
s, err = element.GetString("street")
assert.True(s == "Street 42" && err == nil, "second fail")
@@ -232,6 +235,7 @@ func TestSecond(t *testing.T) {
assert.True(fromName == "Tom Brady" && err == nil, "fromName mismatch")
actions, err := dataItem.GetObjectArray("actions")
assert.True(err == nil, "get object from array failed")
for index, action := range actions {
@@ -225,7 +225,7 @@ func (a *Auth) SignRequest(req *http.Request) {
)
decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
sha256 := hmac.New(sha256.New, []byte(decodedKey))
sha256 := hmac.New(sha256.New, decodedKey)
sha256.Write([]byte(strToSign))
signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
@@ -10,9 +10,11 @@ import (
func TestUploadToAzureBlob(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.azure_blob", t, func() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
err := cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
So(err, ShouldBeNil)
uploader, _ := NewImageUploader()
@@ -10,7 +10,8 @@ import (
func TestUploadToGCS(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.gcs", t, func() {
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
+6 -5
View File
@@ -3,9 +3,10 @@ package imguploader
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/log"
"regexp"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
)
@@ -24,7 +25,7 @@ func NewImageUploader() (ImageUploader, error) {
switch setting.ImageUploadProvider {
case "s3":
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
if err != nil {
return nil, err
}
@@ -51,7 +52,7 @@ func NewImageUploader() (ImageUploader, error) {
return NewS3Uploader(region, bucket, path, "public-read", accessKey, secretKey), nil
case "webdav":
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
webdavSec, err := setting.Raw.GetSection("external_image_storage.webdav")
if err != nil {
return nil, err
}
@@ -67,7 +68,7 @@ func NewImageUploader() (ImageUploader, error) {
return NewWebdavImageUploader(url, username, password, public_url)
case "gcs":
gcssec, err := setting.Cfg.GetSection("external_image_storage.gcs")
gcssec, err := setting.Raw.GetSection("external_image_storage.gcs")
if err != nil {
return nil, err
}
@@ -78,7 +79,7 @@ func NewImageUploader() (ImageUploader, error) {
return NewGCSUploader(keyFile, bucketName, path), nil
case "azure_blob":
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
azureBlobSec, err := setting.Raw.GetSection("external_image_storage.azure_blob")
if err != nil {
return nil, err
}
+25 -17
View File
@@ -11,14 +11,16 @@ import (
func TestImageUploaderFactory(t *testing.T) {
Convey("Can create image uploader for ", t, func() {
Convey("S3ImageUploader config", func() {
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "s3"
Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
@@ -36,7 +38,8 @@ func TestImageUploaderFactory(t *testing.T) {
})
Convey("with bucket url https://s3.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
@@ -54,16 +57,16 @@ func TestImageUploaderFactory(t *testing.T) {
})
Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*S3Uploader)
original, ok := uploader.(*S3Uploader)
So(ok, ShouldBeTrue)
So(original.region, ShouldEqual, "us-west-2")
So(original.bucket, ShouldEqual, "my.bucket.com")
@@ -75,13 +78,15 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("Webdav uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "webdav"
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
webdavSec, err := cfg.Raw.GetSection("external_image_storage.webdav")
So(err, ShouldBeNil)
webdavSec.NewKey("url", "webdavUrl")
webdavSec.NewKey("username", "username")
webdavSec.NewKey("password", "password")
@@ -100,43 +105,45 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("GCS uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "gcs"
gcpSec, err := setting.Cfg.GetSection("external_image_storage.gcs")
gcpSec, err := cfg.Raw.GetSection("external_image_storage.gcs")
So(err, ShouldBeNil)
gcpSec.NewKey("key_file", "/etc/secrets/project-79a52befa3f6.json")
gcpSec.NewKey("bucket", "project-grafana-east")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*GCSUploader)
original, ok := uploader.(*GCSUploader)
So(ok, ShouldBeTrue)
So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json")
So(original.bucket, ShouldEqual, "project-grafana-east")
})
Convey("AzureBlobUploader config", func() {
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "azure_blob"
Convey("with container name", func() {
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
azureBlobSec, err := cfg.Raw.GetSection("external_image_storage.azure_blob")
So(err, ShouldBeNil)
azureBlobSec.NewKey("account_name", "account_name")
azureBlobSec.NewKey("account_key", "account_key")
azureBlobSec.NewKey("container_name", "container_name")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*AzureBlobUploader)
original, ok := uploader.(*AzureBlobUploader)
So(ok, ShouldBeTrue)
So(original.account_name, ShouldEqual, "account_name")
So(original.account_key, ShouldEqual, "account_key")
@@ -147,7 +154,8 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("Local uploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
@@ -10,7 +10,8 @@ import (
func TestUploadToS3(t *testing.T) {
SkipConvey("[Integration test] for external_image_store.s3", t, func() {
setting.NewConfigContext(&setting.CommandLineArgs{
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
HomePath: "../../../",
})
+7 -1
View File
@@ -41,14 +41,20 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
url.Path = path.Join(url.Path, filename)
imgData, err := ioutil.ReadFile(pa)
if err != nil {
return "", err
}
req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData))
if err != nil {
return "", err
}
if u.username != "" {
req.SetBasicAuth(u.username, u.password)
}
res, err := netClient.Do(req)
if err != nil {
return "", err
}
+10 -1
View File
@@ -50,7 +50,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
}
switch x := v.(type) {
case float64:
f.Float64 = float64(x)
f.Float64 = x
case map[string]interface{}:
err = json.Unmarshal(data, &f.NullFloat64)
case nil:
@@ -106,6 +106,15 @@ func (f Float) String() string {
return fmt.Sprintf("%1.3f", f.Float64)
}
// FullString returns float as string in full precision
func (f Float) FullString() string {
if !f.Valid {
return "null"
}
return fmt.Sprintf("%f", f.Float64)
}
// SetValid changes this Float's value and also sets it to be non-null.
func (f *Float) SetValid(n float64) {
f.Float64 = n
+3
View File
@@ -0,0 +1,3 @@
package extensions
import _ "github.com/pkg/errors"
+1 -4
View File
@@ -99,10 +99,7 @@ func (w *FileLogWriter) StartLogger() error {
return err
}
w.mw.SetFd(fd)
if err = w.initFd(); err != nil {
return err
}
return nil
return w.initFd()
}
func (w *FileLogWriter) docheck(size int) {
+2
View File
@@ -32,7 +32,9 @@ func TestLogFile(t *testing.T) {
Convey("Logging should add lines", func() {
err := fileLogWrite.WriteLine("test1\n")
So(err, ShouldBeNil)
err = fileLogWrite.WriteLine("test2\n")
So(err, ShouldBeNil)
err = fileLogWrite.WriteLine("test3\n")
So(err, ShouldBeNil)
So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)
+9 -10
View File
@@ -8,23 +8,22 @@ import (
)
var (
ErrInvalidCredentials = errors.New("Invalid Username or Password")
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
ErrInvalidCredentials = errors.New("Invalid Username or Password")
ErrNoEmail = errors.New("Login provider didn't return an email address")
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
ErrUsersQuotaReached = errors.New("Users quota reached")
ErrGettingUserQuota = errors.New("Error getting user quota")
)
type LoginUserQuery struct {
Username string
Password string
User *m.User
IpAddress string
}
func Init() {
bus.AddHandler("auth", AuthenticateUser)
loadLdapConfig()
}
func AuthenticateUser(query *LoginUserQuery) error {
func AuthenticateUser(query *m.LoginUserQuery) error {
if err := validateLoginAttempts(query.Username); err != nil {
return err
}
+5 -5
View File
@@ -151,7 +151,7 @@ func TestAuthenticateUser(t *testing.T) {
}
type authScenarioContext struct {
loginUserQuery *LoginUserQuery
loginUserQuery *m.LoginUserQuery
grafanaLoginWasCalled bool
ldapLoginWasCalled bool
loginAttemptValidationWasCalled bool
@@ -161,14 +161,14 @@ type authScenarioContext struct {
type authScenarioFunc func(sc *authScenarioContext)
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
loginUsingGrafanaDB = func(query *LoginUserQuery) error {
loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
sc.grafanaLoginWasCalled = true
return err
}
}
func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
sc.ldapLoginWasCalled = true
return enabled, err
}
@@ -182,7 +182,7 @@ func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
}
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
saveInvalidLoginAttempt = func(query *LoginUserQuery) {
saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
sc.saveInvalidLoginAttemptWasCalled = true
}
}
@@ -195,7 +195,7 @@ func authScenario(desc string, fn authScenarioFunc) {
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
sc := &authScenarioContext{
loginUserQuery: &LoginUserQuery{
loginUserQuery: &m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
+3 -3
View File
@@ -9,8 +9,8 @@ import (
)
var (
maxInvalidLoginAttempts int64 = 5
loginAttemptsWindow time.Duration = time.Minute * 5
maxInvalidLoginAttempts int64 = 5
loginAttemptsWindow = time.Minute * 5
)
var validateLoginAttempts = func(username string) error {
@@ -34,7 +34,7 @@ var validateLoginAttempts = func(username string) error {
return nil
}
var saveInvalidLoginAttempt = func(query *LoginUserQuery) {
var saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
if setting.DisableBruteForceLoginProtection {
return
}
@@ -50,7 +50,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
return nil
})
saveInvalidLoginAttempt(&LoginUserQuery{
saveInvalidLoginAttempt(&m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
@@ -103,7 +103,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
return nil
})
saveInvalidLoginAttempt(&LoginUserQuery{
saveInvalidLoginAttempt(&m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
+184
View File
@@ -0,0 +1,184 @@
package login
import (
"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/quota"
)
func init() {
bus.AddHandler("auth", UpsertUser)
}
func UpsertUser(cmd *m.UpsertUserCommand) error {
extUser := cmd.ExternalUser
userQuery := &m.GetUserByAuthInfoQuery{
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
UserId: extUser.UserId,
Email: extUser.Email,
Login: extUser.Login,
}
err := bus.Dispatch(userQuery)
if err != m.ErrUserNotFound && err != nil {
return err
}
if err != nil {
if !cmd.SignupAllowed {
log.Warn("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule)
return ErrInvalidCredentials
}
limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
if err != nil {
log.Warn("Error getting user quota", "err", err)
return ErrGettingUserQuota
}
if limitReached {
return ErrUsersQuotaReached
}
cmd.Result, err = createUser(extUser)
if err != nil {
return err
}
if extUser.AuthModule != "" && extUser.AuthId != "" {
cmd2 := &m.SetAuthInfoCommand{
UserId: cmd.Result.Id,
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
}
if err := bus.Dispatch(cmd2); err != nil {
return err
}
}
} else {
cmd.Result = userQuery.Result
err = updateUser(cmd.Result, extUser)
if err != nil {
return err
}
}
return syncOrgRoles(cmd.Result, extUser)
}
func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
cmd := &m.CreateUserCommand{
Login: extUser.Login,
Email: extUser.Email,
Name: extUser.Name,
SkipOrgSetup: len(extUser.OrgRoles) > 0,
}
if err := bus.Dispatch(cmd); err != nil {
return nil, err
}
return &cmd.Result, nil
}
func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
// sync user info
updateCmd := &m.UpdateUserCommand{
UserId: user.Id,
}
needsUpdate := false
if extUser.Login != "" && extUser.Login != user.Login {
updateCmd.Login = extUser.Login
user.Login = extUser.Login
needsUpdate = true
}
if extUser.Email != "" && extUser.Email != user.Email {
updateCmd.Email = extUser.Email
user.Email = extUser.Email
needsUpdate = true
}
if extUser.Name != "" && extUser.Name != user.Name {
updateCmd.Name = extUser.Name
user.Name = extUser.Name
needsUpdate = true
}
if !needsUpdate {
return nil
}
log.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
return bus.Dispatch(updateCmd)
}
func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
// don't sync org roles if none are specified
if len(extUser.OrgRoles) == 0 {
return nil
}
orgsQuery := &m.GetUserOrgListQuery{UserId: user.Id}
if err := bus.Dispatch(orgsQuery); err != nil {
return err
}
handledOrgIds := map[int64]bool{}
deleteOrgIds := []int64{}
// update existing org roles
for _, org := range orgsQuery.Result {
handledOrgIds[org.OrgId] = true
if extUser.OrgRoles[org.OrgId] == "" {
deleteOrgIds = append(deleteOrgIds, org.OrgId)
} else if extUser.OrgRoles[org.OrgId] != org.Role {
// update role
cmd := &m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extUser.OrgRoles[org.OrgId]}
if err := bus.Dispatch(cmd); err != nil {
return err
}
}
}
// add any new org roles
for orgId, orgRole := range extUser.OrgRoles {
if _, exists := handledOrgIds[orgId]; exists {
continue
}
// add role
cmd := &m.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId}
err := bus.Dispatch(cmd)
if err != nil && err != m.ErrOrgNotFound {
return err
}
}
// delete any removed org roles
for _, orgId := range deleteOrgIds {
cmd := &m.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id}
if err := bus.Dispatch(cmd); err != nil {
return err
}
}
// update user's default org if needed
if _, ok := extUser.OrgRoles[user.OrgId]; !ok {
for orgId := range extUser.OrgRoles {
user.OrgId = orgId
break
}
return bus.Dispatch(&m.SetUsingOrgCommand{
UserId: user.Id,
OrgId: user.OrgId,
})
}
return nil
}
+1 -1
View File
@@ -17,7 +17,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa
return nil
}
var loginUsingGrafanaDB = func(query *LoginUserQuery) error {
var loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
if err := bus.Dispatch(&userQuery); err != nil {
+2 -2
View File
@@ -66,7 +66,7 @@ func TestGrafanaLogin(t *testing.T) {
}
type grafanaLoginScenarioContext struct {
loginUserQuery *LoginUserQuery
loginUserQuery *m.LoginUserQuery
validatePasswordCalled bool
}
@@ -77,7 +77,7 @@ func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) {
origValidatePassword := validatePassword
sc := &grafanaLoginScenarioContext{
loginUserQuery: &LoginUserQuery{
loginUserQuery: &m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
+84 -185
View File
@@ -24,10 +24,9 @@ type ILdapConn interface {
}
type ILdapAuther interface {
Login(query *LoginUserQuery) error
SyncSignedInUser(signedInUser *m.SignedInUser) error
GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error
Login(query *m.LoginUserQuery) error
SyncUser(query *m.LoginUserQuery) error
GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
}
type ldapAuther struct {
@@ -51,12 +50,12 @@ func (a *ldapAuther) Dial() error {
if a.server.RootCACert != "" {
certPool = x509.NewCertPool()
for _, caCertFile := range strings.Split(a.server.RootCACert, " ") {
if pem, err := ioutil.ReadFile(caCertFile); err != nil {
pem, err := ioutil.ReadFile(caCertFile)
if err != nil {
return err
} else {
if !certPool.AppendCertsFromPEM(pem) {
return errors.New("Failed to append CA certificate " + caCertFile)
}
}
if !certPool.AppendCertsFromPEM(pem) {
return errors.New("Failed to append CA certificate " + caCertFile)
}
}
}
@@ -89,7 +88,8 @@ func (a *ldapAuther) Dial() error {
return err
}
func (a *ldapAuther) Login(query *LoginUserQuery) error {
func (a *ldapAuther) Login(query *m.LoginUserQuery) error {
// connect to ldap server
if err := a.Dial(); err != nil {
return err
}
@@ -101,206 +101,105 @@ func (a *ldapAuther) Login(query *LoginUserQuery) error {
}
// find user entry & attributes
if ldapUser, err := a.searchForUser(query.Username); err != nil {
ldapUser, err := a.searchForUser(query.Username)
if err != nil {
return err
} else {
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
}
// check if a second user bind is needed
if a.requireSecondBind {
if err := a.secondBind(ldapUser, query.Password); err != nil {
return err
}
}
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
if grafanaUser, err := a.GetGrafanaUserFor(ldapUser); err != nil {
// check if a second user bind is needed
if a.requireSecondBind {
err = a.secondBind(ldapUser, query.Password)
if err != nil {
return err
} else {
if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil {
return syncErr
}
query.User = grafanaUser
return nil
}
}
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
func (a *ldapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
grafanaUser := m.User{
Id: signedInUser.UserId,
Login: signedInUser.Login,
Email: signedInUser.Email,
Name: signedInUser.Name,
}
if err := a.Dial(); err != nil {
func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error {
// connect to ldap server
err := a.Dial()
if err != nil {
return err
}
defer a.conn.Close()
if err := a.serverBind(); err != nil {
err = a.serverBind()
if err != nil {
return err
}
if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil {
// find user entry & attributes
ldapUser, err := a.searchForUser(query.Username)
if err != nil {
a.log.Error("Failed searching for user in ldap", "error", err)
return err
} else {
if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil {
return err
}
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
extUser := &m.ExternalUserInfo{
AuthModule: "ldap",
AuthId: ldapUser.DN,
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
Login: ldapUser.Username,
Email: ldapUser.Email,
OrgRoles: map[int64]m.RoleType{},
}
for _, group := range a.server.LdapGroups {
// only use the first match for each org
if extUser.OrgRoles[group.OrgId] != "" {
continue
}
a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser))
if ldapUser.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
}
}
return nil
}
// Sync info for ldap user and grafana user
func (a *ldapAuther) syncInfoAndOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
// sync user details
if err := a.syncUserInfo(user, ldapUser); err != nil {
return err
}
// sync org roles
if err := a.SyncOrgRoles(user, ldapUser); err != nil {
return err
}
return nil
}
func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
// validate that the user has access
// if there are no ldap group mappings access is true
// otherwise a single group must match
access := len(a.server.LdapGroups) == 0
for _, ldapGroup := range a.server.LdapGroups {
if ldapUser.isMemberOf(ldapGroup.GroupDN) {
access = true
break
}
}
if !access {
a.log.Info("Ldap Auth: user does not belong in any of the specified ldap groups", "username", ldapUser.Username, "groups", ldapUser.MemberOf)
if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 {
a.log.Info(
"Ldap Auth: user does not belong in any of the specified ldap groups",
"username", ldapUser.Username,
"groups", ldapUser.MemberOf)
return nil, ErrInvalidCredentials
}
// get user from grafana db
userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username}
if err := bus.Dispatch(&userQuery); err != nil {
if err == m.ErrUserNotFound && setting.LdapAllowSignup {
return a.createGrafanaUser(ldapUser)
} else if err == m.ErrUserNotFound {
a.log.Warn("Not allowing LDAP login, user not found in internal user database, and ldap allow signup = false")
return nil, ErrInvalidCredentials
} else {
return nil, err
}
// add/update user in grafana
userQuery := &m.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: setting.LdapAllowSignup,
}
return userQuery.Result, nil
}
func (a *ldapAuther) createGrafanaUser(ldapUser *LdapUserInfo) (*m.User, error) {
cmd := m.CreateUserCommand{
Login: ldapUser.Username,
Email: ldapUser.Email,
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
}
if err := bus.Dispatch(&cmd); err != nil {
err := bus.Dispatch(userQuery)
if err != nil {
return nil, err
}
return &cmd.Result, nil
}
func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *LdapUserInfo) error {
var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
if user.Email == ldapUser.Email && user.Name == name {
return nil
}
a.log.Debug("Syncing user info", "username", ldapUser.Username)
updateCmd := m.UpdateUserCommand{}
updateCmd.UserId = user.Id
updateCmd.Login = user.Login
updateCmd.Email = ldapUser.Email
updateCmd.Name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
return bus.Dispatch(&updateCmd)
}
func (a *ldapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
if len(a.server.LdapGroups) == 0 {
a.log.Warn("No group mappings defined")
return nil
}
orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
if err := bus.Dispatch(&orgsQuery); err != nil {
return err
}
handledOrgIds := map[int64]bool{}
// update or remove org roles
for _, org := range orgsQuery.Result {
match := false
handledOrgIds[org.OrgId] = true
for _, group := range a.server.LdapGroups {
if org.OrgId != group.OrgId {
continue
}
if ldapUser.isMemberOf(group.GroupDN) {
match = true
if org.Role != group.OrgRole {
// update role
cmd := m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: group.OrgRole}
if err := bus.Dispatch(&cmd); err != nil {
return err
}
}
// ignore subsequent ldap group mapping matches
break
}
}
// remove role if no mappings match
if !match {
cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id}
if err := bus.Dispatch(&cmd); err != nil {
return err
}
}
}
// add missing org roles
for _, group := range a.server.LdapGroups {
if !ldapUser.isMemberOf(group.GroupDN) {
continue
}
if _, exists := handledOrgIds[group.OrgId]; exists {
continue
}
// add role
cmd := m.AddOrgUserCommand{UserId: user.Id, Role: group.OrgRole, OrgId: group.OrgId}
err := bus.Dispatch(&cmd)
if err != nil && err != m.ErrOrgNotFound {
return err
}
// mark this group has handled so we do not process it again
handledOrgIds[group.OrgId] = true
}
return nil
return userQuery.Result, nil
}
func (a *ldapAuther) serverBind() error {
@@ -404,9 +303,10 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
var groupSearchResult *ldap.SearchResult
for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
var filter_replace string
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
if a.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
} else {
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
}
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
@@ -448,6 +348,9 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
}
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
if name == "DN" {
return result.Entries[0].DN
}
for _, attr := range result.Entries[n].Attributes {
if attr.Name == name {
if len(attr.Values) > 0 {
@@ -470,7 +373,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
}
return []string{}
}
func createUserFromLdapInfo() error {
return nil
}
+2 -1
View File
@@ -1,10 +1,11 @@
package login
import (
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
var loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
var loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
if !setting.LdapEnabled {
return false, nil
}
+6 -10
View File
@@ -79,7 +79,7 @@ func TestLdapLogin(t *testing.T) {
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
sc.withLoginResult(false)
enabled, err := loginUsingLdap(&LoginUserQuery{
enabled, err := loginUsingLdap(&m.LoginUserQuery{
Username: "user",
Password: "pwd",
})
@@ -117,7 +117,7 @@ type mockLdapAuther struct {
loginCalled bool
}
func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error {
a.loginCalled = true
if !a.validLogin {
@@ -127,20 +127,16 @@ func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
return nil
}
func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error {
return nil
}
func (a *mockLdapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
func (a *mockLdapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
return nil, nil
}
func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
return nil
}
type ldapLoginScenarioContext struct {
loginUserQuery *LoginUserQuery
loginUserQuery *m.LoginUserQuery
ldapAuthenticatorMock *mockLdapAuther
}
@@ -151,7 +147,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
origNewLdapAuthenticator := NewLdapAuthenticator
sc := &ldapLoginScenarioContext{
loginUserQuery: &LoginUserQuery{
loginUserQuery: &m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
+78 -34
View File
@@ -18,7 +18,7 @@ func TestLdapAuther(t *testing.T) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{{}},
})
_, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
So(err, ShouldEqual, ErrInvalidCredentials)
})
@@ -34,7 +34,7 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(user1)
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
@@ -48,7 +48,21 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(user1)
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{MemberOf: []string{"cn=users"}})
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"cn=users"}})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
ldapAutherScenario("Given group match with different case", func(sc *scenarioContext) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
{GroupDN: "cn=users", OrgRole: "Admin"},
},
})
sc.userQueryReturns(user1)
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"CN=users"}})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
@@ -64,7 +78,8 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(nil)
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
DN: "torkelo",
Username: "torkelo",
Email: "my@email.com",
MemberOf: []string{"cn=editor"},
@@ -72,11 +87,6 @@ func TestLdapAuther(t *testing.T) {
So(err, ShouldBeNil)
Convey("Should create new user", func() {
So(sc.createUserCmd.Login, ShouldEqual, "torkelo")
So(sc.createUserCmd.Email, ShouldEqual, "my@email.com")
})
Convey("Should return new user", func() {
So(result.Login, ShouldEqual, "torkelo")
})
@@ -95,7 +105,7 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=users"},
})
@@ -114,7 +124,7 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=users"},
})
@@ -122,24 +132,29 @@ func TestLdapAuther(t *testing.T) {
So(err, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldNotBeNil)
So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
{GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
{GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
MemberOf: []string{"cn=other"},
sc.userOrgsQueryReturns([]*m.UserOrgDTO{
{OrgId: 1, Role: m.ROLE_EDITOR},
{OrgId: 2, Role: m.ROLE_EDITOR},
})
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=users"},
})
Convey("Should remove org role", func() {
So(err, ShouldBeNil)
So(sc.removeOrgUserCmd, ShouldNotBeNil)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
})
})
@@ -152,7 +167,7 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=users"},
})
@@ -160,6 +175,7 @@ func TestLdapAuther(t *testing.T) {
So(err, ShouldBeNil)
So(sc.removeOrgUserCmd, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldNotBeNil)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
@@ -172,13 +188,14 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should take first match, and ignore subsequent matches", func() {
So(err, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldBeNil)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
@@ -191,19 +208,20 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should take first match, and ignore subsequent matches", func() {
So(err, ShouldBeNil)
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
})
Convey("When calling SyncSignedInUser", t, func() {
Convey("When calling SyncUser", t, func() {
mockLdapConnection := &mockLdapConn{}
ldapAuther := NewLdapAuthenticator(
@@ -243,17 +261,20 @@ func TestLdapAuther(t *testing.T) {
ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
// arrange
signedInUser := &m.SignedInUser{
Email: "roel@test.net",
UserId: 1,
Name: "Roel Gerrits",
Login: "roelgerrits",
query := &m.LoginUserQuery{
Username: "roelgerrits",
}
sc.userQueryReturns(&m.User{
Id: 1,
Email: "roel@test.net",
Name: "Roel Gerrits",
Login: "roelgerrits",
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
// act
syncErrResult := ldapAuther.SyncSignedInUser(signedInUser)
syncErrResult := ldapAuther.SyncUser(query)
// assert
So(dialCalled, ShouldBeTrue)
@@ -299,6 +320,19 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
sc := &scenarioContext{}
bus.AddHandler("test", UpsertUser)
bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
sc.getUserByAuthInfoQuery = cmd
sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
return nil
})
bus.AddHandler("test", func(cmd *m.GetUserOrgListQuery) error {
sc.getUserOrgListQuery = cmd
return nil
})
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
sc.createUserCmd = cmd
sc.createUserCmd.Result = m.User{Login: cmd.Login}
@@ -325,26 +359,36 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
return nil
})
bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error {
sc.setUsingOrgCmd = cmd
return nil
})
fn(sc)
})
}
type scenarioContext struct {
createUserCmd *m.CreateUserCommand
addOrgUserCmd *m.AddOrgUserCommand
updateOrgUserCmd *m.UpdateOrgUserCommand
removeOrgUserCmd *m.RemoveOrgUserCommand
updateUserCmd *m.UpdateUserCommand
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
getUserOrgListQuery *m.GetUserOrgListQuery
createUserCmd *m.CreateUserCommand
addOrgUserCmd *m.AddOrgUserCommand
updateOrgUserCmd *m.UpdateOrgUserCommand
removeOrgUserCmd *m.RemoveOrgUserCommand
updateUserCmd *m.UpdateUserCommand
setUsingOrgCmd *m.SetUsingOrgCommand
}
func (sc *scenarioContext) userQueryReturns(user *m.User) {
bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error {
if user == nil {
return m.ErrUserNotFound
} else {
query.Result = user
return nil
}
query.Result = user
return nil
})
bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error {
return nil
})
}
+5 -1
View File
@@ -1,5 +1,9 @@
package login
import (
"strings"
)
type LdapUserInfo struct {
DN string
FirstName string
@@ -15,7 +19,7 @@ func (u *LdapUserInfo) isMemberOf(group string) bool {
}
for _, member := range u.MemberOf {
if member == group {
if strings.EqualFold(member, group) {
return true
}
}
+3 -7
View File
@@ -55,7 +55,7 @@ const (
AbortOnError
)
var metricCategoryPrefix []string = []string{
var metricCategoryPrefix = []string{
"proxy_",
"api_",
"page_",
@@ -66,7 +66,7 @@ var metricCategoryPrefix []string = []string{
"go_",
"process_"}
var trimMetricPrefix []string = []string{"grafana_"}
var trimMetricPrefix = []string{"grafana_"}
// Config defines the Graphite bridge config.
type Config struct {
@@ -295,11 +295,7 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error
}
}
if err = addExtentionConventionForRollups(buf, mf, m); err != nil {
return err
}
return nil
return addExtentionConventionForRollups(buf, mf, m)
}
func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {
-38
View File
@@ -1,38 +0,0 @@
package metrics
import (
"context"
ini "gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
)
var metricsLogger log.Logger = log.New("metrics")
type logWrapper struct {
logger log.Logger
}
func (lw *logWrapper) Println(v ...interface{}) {
lw.logger.Info("graphite metric bridge", v...)
}
func Init(file *ini.File) {
cfg := ReadSettings(file)
internalInit(cfg)
}
func internalInit(settings *MetricSettings) {
initMetricVars(settings)
if settings.GraphiteBridgeConfig != nil {
bridge, err := graphitebridge.NewBridge(settings.GraphiteBridgeConfig)
if err != nil {
metricsLogger.Error("failed to create graphite bridge", "error", err)
} else {
go bridge.Run(context.Background())
}
}
}
+20 -34
View File
@@ -54,6 +54,7 @@ var (
M_Alerting_Active_Alerts prometheus.Gauge
M_StatTotal_Dashboards prometheus.Gauge
M_StatTotal_Users prometheus.Gauge
M_StatActive_Users prometheus.Gauge
M_StatTotal_Orgs prometheus.Gauge
M_StatTotal_Playlists prometheus.Gauge
M_Grafana_Version *prometheus.GaugeVec
@@ -253,6 +254,12 @@ func init() {
Namespace: exporterName,
})
M_StatActive_Users = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_active_users",
Help: "number of active users",
Namespace: exporterName,
})
M_StatTotal_Orgs = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_total_orgs",
Help: "total amount of orgs",
@@ -270,10 +277,9 @@ func init() {
Help: "Information about the Grafana",
Namespace: exporterName,
}, []string{"version"})
}
func initMetricVars(settings *MetricSettings) {
func initMetricVars() {
prometheus.MustRegister(
M_Instance_Start,
M_Page_Status,
@@ -305,45 +311,25 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Active_Alerts,
M_StatTotal_Dashboards,
M_StatTotal_Users,
M_StatActive_Users,
M_StatTotal_Orgs,
M_StatTotal_Playlists,
M_Grafana_Version)
go instrumentationLoop(settings)
}
func instrumentationLoop(settings *MetricSettings) chan struct{} {
M_Instance_Start.Inc()
onceEveryDayTick := time.NewTicker(time.Hour * 24)
secondTicker := time.NewTicker(time.Second * time.Duration(settings.IntervalSeconds))
for {
select {
case <-onceEveryDayTick.C:
sendUsageStats()
case <-secondTicker.C:
updateTotalStats()
}
}
}
var metricPublishCounter int64 = 0
func updateTotalStats() {
metricPublishCounter++
if metricPublishCounter == 1 || metricPublishCounter%10 == 0 {
statsQuery := models.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
statsQuery := models.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
}
func sendUsageStats() {
@@ -403,6 +389,6 @@ func sendUsageStats() {
out, _ := json.MarshalIndent(report, "", " ")
data := bytes.NewBuffer(out)
client := http.Client{Timeout: time.Duration(5 * time.Second)}
client := http.Client{Timeout: 5 * time.Second}
go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data)
}
+71
View File
@@ -0,0 +1,71 @@
package metrics
import (
"context"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
var metricsLogger log.Logger = log.New("metrics")
type logWrapper struct {
logger log.Logger
}
func (lw *logWrapper) Println(v ...interface{}) {
lw.logger.Info("graphite metric bridge", v...)
}
func init() {
registry.RegisterService(&InternalMetricsService{})
initMetricVars()
}
type InternalMetricsService struct {
Cfg *setting.Cfg `inject:""`
enabled bool
intervalSeconds int64
graphiteCfg *graphitebridge.Config
}
func (im *InternalMetricsService) Init() error {
return im.readSettings()
}
func (im *InternalMetricsService) Run(ctx context.Context) error {
// Start Graphite Bridge
if im.graphiteCfg != nil {
bridge, err := graphitebridge.NewBridge(im.graphiteCfg)
if err != nil {
metricsLogger.Error("failed to create graphite bridge", "error", err)
} else {
go bridge.Run(ctx)
}
}
M_Instance_Start.Inc()
// set the total stats gauges before we publishing metrics
updateTotalStats()
onceEveryDayTick := time.NewTicker(time.Hour * 24)
everyMinuteTicker := time.NewTicker(time.Minute)
defer onceEveryDayTick.Stop()
defer everyMinuteTicker.Stop()
for {
select {
case <-onceEveryDayTick.C:
sendUsageStats()
case <-everyMinuteTicker.C:
updateTotalStats()
case <-ctx.Done():
return ctx.Err()
}
}
}
+23 -35
View File
@@ -1,67 +1,53 @@
package metrics
import (
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
ini "gopkg.in/ini.v1"
)
type MetricSettings struct {
Enabled bool
IntervalSeconds int64
GraphiteBridgeConfig *graphitebridge.Config
}
func ReadSettings(file *ini.File) *MetricSettings {
var settings = &MetricSettings{
Enabled: false,
func (im *InternalMetricsService) readSettings() error {
var section, err = im.Cfg.Raw.GetSection("metrics")
if err != nil {
return fmt.Errorf("Unable to find metrics config section %v", err)
}
var section, err = file.GetSection("metrics")
if err != nil {
metricsLogger.Crit("Unable to find metrics config section", "error", err)
im.enabled = section.Key("enabled").MustBool(false)
im.intervalSeconds = section.Key("interval_seconds").MustInt64(10)
if !im.enabled {
return nil
}
settings.Enabled = section.Key("enabled").MustBool(false)
settings.IntervalSeconds = section.Key("interval_seconds").MustInt64(10)
if !settings.Enabled {
return settings
if err := im.parseGraphiteSettings(); err != nil {
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
}
cfg, err := parseGraphiteSettings(settings, file)
if err != nil {
metricsLogger.Crit("Unable to parse metrics graphite section", "error", err)
return nil
}
settings.GraphiteBridgeConfig = cfg
return settings
return nil
}
func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphitebridge.Config, error) {
graphiteSection, err := setting.Cfg.GetSection("metrics.graphite")
func (im *InternalMetricsService) parseGraphiteSettings() error {
graphiteSection, err := im.Cfg.Raw.GetSection("metrics.graphite")
if err != nil {
return nil, nil
return nil
}
address := graphiteSection.Key("address").String()
if address == "" {
return nil, nil
return nil
}
cfg := &graphitebridge.Config{
bridgeCfg := &graphitebridge.Config{
URL: address,
Prefix: graphiteSection.Key("prefix").MustString("prod.grafana.%(instance_name)s"),
CountersAsDelta: true,
Gatherer: prometheus.DefaultGatherer,
Interval: time.Duration(settings.IntervalSeconds) * time.Second,
Interval: time.Duration(im.intervalSeconds) * time.Second,
Timeout: 10 * time.Second,
Logger: &logWrapper{logger: metricsLogger},
ErrorHandling: graphitebridge.ContinueOnError,
@@ -74,6 +60,8 @@ func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphiteb
prefix = "prod.grafana.%(instance_name)s."
}
cfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1)
return cfg, nil
bridgeCfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1)
im.graphiteCfg = bridgeCfg
return nil
}
+127 -88
View File
@@ -1,8 +1,10 @@
package middleware
import (
"errors"
"fmt"
"net"
"net/mail"
"reflect"
"strings"
"time"
@@ -14,6 +16,8 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
if !setting.AuthProxyEnabled {
return false
@@ -25,45 +29,119 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
}
// if auth proxy ip(s) defined, check if request comes from one of those
if err := checkAuthenticationProxy(ctx, proxyHeaderValue); err != nil {
if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
ctx.Handle(407, "Proxy authentication required", err)
return true
}
query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
query.OrgId = orgID
if err := bus.Dispatch(query); err != nil {
if err != m.ErrUserNotFound {
ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
return true
}
if !setting.AuthProxyAutoSignUp {
return false
}
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
if setting.LdapEnabled {
cmd.SkipOrgSetup = true
}
if err := bus.Dispatch(cmd); err != nil {
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
return true
}
query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgID}
if err := bus.Dispatch(query); err != nil {
ctx.Handle(500, "Failed find user after creation", err)
return true
}
}
// initialize session
if err := ctx.Session.Start(ctx.Context); err != nil {
log.Error(3, "Failed to start session", err)
return false
}
query := &m.GetSignedInUserQuery{OrgId: orgID}
// if this session has already been authenticated by authProxy just load the user
sessProxyValue := ctx.Session.Get(AUTH_PROXY_SESSION_VAR)
if sessProxyValue != nil && sessProxyValue.(string) == proxyHeaderValue && getRequestUserId(ctx) > 0 {
// if we're using ldap, sync user periodically
if setting.LdapEnabled {
syncQuery := &m.LoginUserQuery{
ReqContext: ctx,
Username: proxyHeaderValue,
}
if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
if err == login.ErrInvalidCredentials {
ctx.Handle(500, "Unable to authenticate user", err)
return false
}
ctx.Handle(500, "Failed to sync user", err)
return false
}
}
query.UserId = getRequestUserId(ctx)
// if we're using ldap, pass authproxy login name to ldap user sync
} else if setting.LdapEnabled {
ctx.Session.Delete(session.SESS_KEY_LASTLDAPSYNC)
syncQuery := &m.LoginUserQuery{
ReqContext: ctx,
Username: proxyHeaderValue,
}
if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
if err == login.ErrInvalidCredentials {
ctx.Handle(500, "Unable to authenticate user", err)
return false
}
ctx.Handle(500, "Failed to sync user", err)
return false
}
if syncQuery.User == nil {
ctx.Handle(500, "Failed to sync user", nil)
return false
}
query.UserId = syncQuery.User.Id
// no ldap, just use the info we have
} else {
extUser := &m.ExternalUserInfo{
AuthModule: "authproxy",
AuthId: proxyHeaderValue,
}
if setting.AuthProxyHeaderProperty == "username" {
extUser.Login = proxyHeaderValue
// only set Email if it can be parsed as an email address
emailAddr, emailErr := mail.ParseAddress(proxyHeaderValue)
if emailErr == nil {
extUser.Email = emailAddr.Address
}
} else if setting.AuthProxyHeaderProperty == "email" {
extUser.Email = proxyHeaderValue
extUser.Login = proxyHeaderValue
} else {
ctx.Handle(500, "Auth proxy header property invalid", nil)
return true
}
for _, field := range []string{"Name", "Email", "Login"} {
if setting.AuthProxyHeaders[field] == "" {
continue
}
if val := ctx.Req.Header.Get(setting.AuthProxyHeaders[field]); val != "" {
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
}
}
// add/update user in grafana
cmd := &m.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: setting.AuthProxyAutoSignUp,
}
err := bus.Dispatch(cmd)
if err != nil {
ctx.Handle(500, "Failed to login as user specified in auth proxy header", err)
return true
}
query.UserId = cmd.Result.Id
}
if err := bus.Dispatch(query); err != nil {
ctx.Handle(500, "Failed to find user", err)
return true
}
// Make sure that we cannot share a session between different users!
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
// remove session
@@ -77,16 +155,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
}
}
// When ldap is enabled, sync userinfo and org roles
if err := syncGrafanaUserWithLdapUser(ctx, query); err != nil {
if err == login.ErrInvalidCredentials {
ctx.Handle(500, "Unable to authenticate user", err)
return false
}
ctx.Handle(500, "Failed to sync user", err)
return false
}
ctx.Session.Set(AUTH_PROXY_SESSION_VAR, proxyHeaderValue)
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
@@ -95,81 +164,51 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
return true
}
var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
if !setting.LdapEnabled {
return nil
}
var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
var lastLdapSync int64
if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
if lastLdapSyncInSession := query.ReqContext.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
lastLdapSync = lastLdapSyncInSession.(int64)
}
if lastLdapSync < expireEpoch {
ldapCfg := login.LdapCfg
if len(ldapCfg.Servers) < 1 {
return fmt.Errorf("No LDAP servers available")
}
for _, server := range ldapCfg.Servers {
author := login.NewLdapAuthenticator(server)
if err := author.SyncSignedInUser(query.Result); err != nil {
if err := author.SyncUser(query); err != nil {
return err
}
}
ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
query.ReqContext.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
}
return nil
}
func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error {
func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
return nil
}
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
sourceIP := remoteAddrSplit[0]
found := false
for _, proxyIP := range proxies {
if sourceIP == strings.TrimSpace(proxyIP) {
found = true
break
}
}
if !found {
msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
err := errors.New(msg)
sourceIP, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return err
}
return nil
}
func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
query := m.GetSignedInUserQuery{}
if setting.AuthProxyHeaderProperty == "username" {
query.Login = headerVal
} else if setting.AuthProxyHeaderProperty == "email" {
query.Email = headerVal
} else {
panic("Auth proxy header property invalid")
// Compare allowed IP addresses to actual address
for _, proxyIP := range proxies {
if sourceIP == strings.TrimSpace(proxyIP) {
return nil
}
}
return &query
}
func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand {
cmd := m.CreateUserCommand{}
if setting.AuthProxyHeaderProperty == "username" {
cmd.Login = headerVal
cmd.Email = headerVal
} else if setting.AuthProxyHeaderProperty == "email" {
cmd.Email = headerVal
cmd.Login = headerVal
} else {
panic("Auth proxy header property invalid")
}
return &cmd
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
}
+36 -24
View File
@@ -26,57 +26,71 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) {
return &mockLdapAuther
}
signedInUser := m.SignedInUser{}
query := m.GetSignedInUserQuery{Result: &signedInUser}
Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() {
Convey("When user logs in, call SyncUser", func() {
// arrange
sess := mockSession{}
sess := newMockSession()
ctx := m.ReqContext{Session: &sess}
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
// act
syncGrafanaUserWithLdapUser(&ctx, &query)
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
ReqContext: &ctx,
Username: "test",
})
// assert
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0)
})
Convey("When session variable not expired, don't sync and don't change session var", func() {
// arrange
sess := mockSession{}
sess := newMockSession()
ctx := m.ReqContext{Session: &sess}
now := time.Now().Unix()
sess.Set(session.SESS_KEY_LASTLDAPSYNC, now)
sess.Set(AUTH_PROXY_SESSION_VAR, "test")
// act
syncGrafanaUserWithLdapUser(&ctx, &query)
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
ReqContext: &ctx,
Username: "test",
})
// assert
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse)
So(mockLdapAuther.syncUserCalled, ShouldBeFalse)
})
Convey("When lastldapsync is expired, session variable should be updated", func() {
// arrange
sess := mockSession{}
sess := newMockSession()
ctx := m.ReqContext{Session: &sess}
expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime)
sess.Set(AUTH_PROXY_SESSION_VAR, "test")
// act
syncGrafanaUserWithLdapUser(&ctx, &query)
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
ReqContext: &ctx,
Username: "test",
})
// assert
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
})
})
}
type mockSession struct {
value interface{}
value map[interface{}]interface{}
}
func newMockSession() mockSession {
session := mockSession{}
session.value = make(map[interface{}]interface{})
return session
}
func (s *mockSession) Start(c *macaron.Context) error {
@@ -84,15 +98,16 @@ func (s *mockSession) Start(c *macaron.Context) error {
}
func (s *mockSession) Set(k interface{}, v interface{}) error {
s.value = v
s.value[k] = v
return nil
}
func (s *mockSession) Get(k interface{}) interface{} {
return s.value
return s.value[k]
}
func (s *mockSession) Delete(k interface{}) interface{} {
delete(s.value, k)
return nil
}
@@ -113,21 +128,18 @@ func (s *mockSession) RegenerateId(c *macaron.Context) error {
}
type mockLdapAuthenticator struct {
syncSignedInUserCalled bool
syncUserCalled bool
}
func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error {
func (a *mockLdapAuthenticator) Login(query *m.LoginUserQuery) error {
return nil
}
func (a *mockLdapAuthenticator) SyncSignedInUser(signedInUser *m.SignedInUser) error {
a.syncSignedInUserCalled = true
func (a *mockLdapAuthenticator) SyncUser(query *m.LoginUserQuery) error {
a.syncUserCalled = true
return nil
}
func (a *mockLdapAuthenticator) GetGrafanaUserFor(ldapUser *login.LdapUserInfo) (*m.User, error) {
func (a *mockLdapAuthenticator) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *login.LdapUserInfo) (*m.User, error) {
return nil, nil
}
func (a *mockLdapAuthenticator) SyncOrgRoles(user *m.User, ldapUser *login.LdapUserInfo) error {
return nil
}
+1 -2
View File
@@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/log"
l "github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
@@ -165,7 +164,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
user := loginQuery.Result
loginUserQuery := l.LoginUserQuery{Username: username, Password: password, User: user}
loginUserQuery := m.LoginUserQuery{Username: username, Password: password, User: user}
if err := bus.Dispatch(&loginUserQuery); err != nil {
ctx.JsonApiErr(401, "Invalid username or password", err)
return true
+50 -11
View File
@@ -9,7 +9,6 @@ import (
ms "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
l "github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
@@ -72,7 +71,7 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
bus.AddHandler("test", func(loginUserQuery *l.LoginUserQuery) error {
bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error {
return nil
})
@@ -177,12 +176,18 @@ func TestMiddlewareContext(t *testing.T) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.LdapEnabled = false
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 12}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.exec()
@@ -199,18 +204,18 @@ func TestMiddlewareContext(t *testing.T) {
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyAutoSignUp = true
setting.LdapEnabled = false
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
if query.UserId > 0 {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
} else {
return m.ErrUserNotFound
}
return m.ErrUserNotFound
})
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
cmd.Result = m.User{Id: 33}
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
@@ -226,11 +231,11 @@ func TestMiddlewareContext(t *testing.T) {
})
})
middlewareScenario("When auth_proxy is enabled and request RemoteAddr is not trusted", func(sc *scenarioContext) {
middlewareScenario("When auth_proxy is enabled and IPv4 request RemoteAddr is not trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
@@ -239,6 +244,24 @@ func TestMiddlewareContext(t *testing.T) {
Convey("should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.3.1 is not from the authentication proxy")
})
})
middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.RemoteAddr = "[2001:23]:12345"
sc.exec()
Convey("should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 2001:23 is not from the authentication proxy")
})
})
@@ -246,16 +269,21 @@ func TestMiddlewareContext(t *testing.T) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.RemoteAddr = "192.168.2.1:12345"
sc.req.RemoteAddr = "[2001::23]:12345"
sc.exec()
Convey("Should init context with user info", func() {
@@ -271,6 +299,11 @@ func TestMiddlewareContext(t *testing.T) {
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = ""
bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
query.Result = &m.User{Id: 32}
return nil
})
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
return nil
@@ -301,11 +334,17 @@ func TestMiddlewareContext(t *testing.T) {
setting.LdapEnabled = true
called := false
syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
called = true
query.User = &m.User{Id: 32}
return nil
}
bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
query.Result = &m.User{Id: 32}
return nil
})
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
return nil
+1 -1
View File
@@ -35,7 +35,7 @@ var (
slash = []byte("/")
)
// stack returns a nicely formated stack frame, skipping skip frames
// stack returns a nicely formatted stack frame, skipping skip frames
func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently
-2
View File
@@ -31,8 +31,6 @@ func initContextWithRenderAuth(ctx *m.ReqContext) bool {
return true
}
type renderContextFunc func(key string) (string, error)
func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string {
renderKeysLock.Lock()
+2 -2
View File
@@ -34,8 +34,8 @@ const (
)
var (
ErrCannotChangeStateOnPausedAlert error = fmt.Errorf("Cannot change state on pause alert")
ErrRequiresNewState error = fmt.Errorf("update alert state requires a new state.")
ErrCannotChangeStateOnPausedAlert = fmt.Errorf("Cannot change state on pause alert")
ErrRequiresNewState = fmt.Errorf("update alert state requires a new state.")
)
func (s AlertStateType) IsValid() bool {
+4
View File
@@ -56,7 +56,10 @@ type DashboardAclInfoDTO struct {
UserId int64 `json:"userId"`
UserLogin string `json:"userLogin"`
UserEmail string `json:"userEmail"`
UserAvatarUrl string `json:"userAvatarUrl"`
TeamId int64 `json:"teamId"`
TeamEmail string `json:"teamEmail"`
TeamAvatarUrl string `json:"teamAvatarUrl"`
Team string `json:"team"`
Role *RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`
@@ -66,6 +69,7 @@ type DashboardAclInfoDTO struct {
Slug string `json:"slug"`
IsFolder bool `json:"isFolder"`
Url string `json:"url"`
Inherited bool `json:"inherited"`
}
func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool {
+36 -24
View File
@@ -13,26 +13,27 @@ import (
// Typed errors
var (
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardFolderNotFound = errors.New("Folder not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
RootFolderName = "General"
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardFolderNotFound = errors.New("Folder not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
RootFolderName = "General"
)
type UpdatePluginDashboardError struct {
@@ -157,7 +158,7 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
return dash
}
// GetDashboardModel turns the command into the savable model
// GetDashboardModel turns the command into the saveable model
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash := NewDashboardFromJson(cmd.Dashboard)
userId := cmd.UserId
@@ -209,14 +210,14 @@ func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string {
return GetDashboardUrl(uid, slug)
}
// Return the html url for a dashboard
// GetDashboardUrl return the html url for a dashboard
func GetDashboardUrl(uid string, slug string) string {
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
}
// Return the full url for a dashboard
// GetFullDashboardUrl return the full url for a dashboard
func GetFullDashboardUrl(uid string, slug string) string {
return fmt.Sprintf("%s%s", setting.AppUrl, GetDashboardUrl(uid, slug))
return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug)
}
// GetFolderUrl return the html url for a folder
@@ -224,6 +225,10 @@ func GetFolderUrl(folderUid string, slug string) string {
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
}
type ValidateDashboardBeforeSaveResult struct {
IsParentFolderChanged bool
}
//
// COMMANDS
//
@@ -268,6 +273,7 @@ type ValidateDashboardBeforeSaveCommand struct {
OrgId int64
Dashboard *Dashboard
Overwrite bool
Result *ValidateDashboardBeforeSaveResult
}
//
@@ -317,6 +323,12 @@ type GetDashboardSlugByIdQuery struct {
Result string
}
type IsDashboardProvisionedQuery struct {
DashboardId int64
Result bool
}
type GetProvisionedDashboardDataQuery struct {
Name string
+13
View File
@@ -4,11 +4,24 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardModel(t *testing.T) {
Convey("Generate full dashboard url", t, func() {
setting.AppUrl = "http://grafana.local/"
fullUrl := GetFullDashboardUrl("uid", "my-dashboard")
So(fullUrl, ShouldEqual, "http://grafana.local/d/uid/my-dashboard")
})
Convey("Generate relative dashboard url", t, func() {
setting.AppUrl = ""
fullUrl := GetDashboardUrl("uid", "my-dashboard")
So(fullUrl, ShouldEqual, "/d/uid/my-dashboard")
})
Convey("When generating slug", t, func() {
dashboard := NewDashboard("Grafana Play Home")
dashboard.UpdateSlug()
+1 -1
View File
@@ -58,7 +58,7 @@ type DataSource struct {
Updated time.Time
}
var knownDatasourcePlugins map[string]bool = map[string]bool{
var knownDatasourcePlugins = map[string]bool{
DS_ES: true,
DS_GRAPHITE: true,
DS_INFLUXDB: true,
+1 -1
View File
@@ -33,7 +33,7 @@ func (ds *DataSource) GetHttpClient() (*http.Client, error) {
}
return &http.Client{
Timeout: time.Duration(30 * time.Second),
Timeout: 30 * time.Second,
Transport: transport,
}, nil
}
+10 -1
View File
@@ -32,7 +32,7 @@ type Folder struct {
HasAcl bool
}
// GetDashboardModel turns the command into the savable model
// GetDashboardModel turns the command into the saveable model
func (cmd *CreateFolderCommand) GetDashboardModel(orgId int64, userId int64) *Dashboard {
dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title))
dashFolder.OrgId = orgId
@@ -89,3 +89,12 @@ type UpdateFolderCommand struct {
Result *Folder
}
//
// QUERIES
//
type HasEditPermissionInFoldersQuery struct {
SignedInUser *SignedInUser
Result bool
}
+7 -6
View File
@@ -19,12 +19,13 @@ type SendEmailCommandSync struct {
}
type SendWebhookSync 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
}
type SendResetPasswordEmailCommand struct {
+2 -2
View File
@@ -48,9 +48,9 @@ func (r *RoleType) UnmarshalJSON(data []byte) error {
*r = RoleType(str)
if (*r).IsValid() == false {
if !(*r).IsValid() {
if (*r) != "" {
return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r))
return fmt.Errorf("JSON validation error: invalid role value: %s", *r)
}
*r = ROLE_VIEWER
+72
View File
@@ -0,0 +1,72 @@
package models
import (
"time"
)
type UserAuth struct {
Id int64
UserId int64
AuthModule string
AuthId string
Created time.Time
}
type ExternalUserInfo struct {
AuthModule string
AuthId string
UserId int64
Email string
Login string
Name string
OrgRoles map[int64]RoleType
}
// ---------------------
// COMMANDS
type UpsertUserCommand struct {
ReqContext *ReqContext
ExternalUser *ExternalUserInfo
SignupAllowed bool
Result *User
}
type SetAuthInfoCommand struct {
AuthModule string
AuthId string
UserId int64
}
type DeleteAuthInfoCommand struct {
UserAuth *UserAuth
}
// ----------------------
// QUERIES
type LoginUserQuery struct {
ReqContext *ReqContext
Username string
Password string
User *User
IpAddress string
}
type GetUserByAuthInfoQuery struct {
AuthModule string
AuthId string
UserId int64
Email string
Login string
Result *User
}
type GetAuthInfoQuery struct {
AuthModule string
AuthId string
Result *UserAuth
}
+3 -3
View File
@@ -148,11 +148,11 @@ func (this *DashTemplateEvaluator) evalValue(source *simplejson.Json) interface{
switch v := sourceValue.(type) {
case string:
interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string {
if replacement, exists := this.variables[match]; exists {
replacement, exists := this.variables[match]
if exists {
return replacement
} else {
return match
}
return match
})
return interpolated
case bool:
+5 -5
View File
@@ -1,7 +1,6 @@
package plugins
import (
"context"
"io/ioutil"
"testing"
@@ -88,13 +87,14 @@ func TestDashboardImport(t *testing.T) {
func pluginScenario(desc string, t *testing.T, fn func()) {
Convey("Given a plugin", t, func() {
setting.Cfg = ini.Empty()
sec, _ := setting.Cfg.NewSection("plugin.test-app")
setting.Raw = ini.Empty()
sec, _ := setting.Raw.NewSection("plugin.test-app")
sec.NewKey("path", "../../tests/test-app")
err := initPlugins(context.Background())
pm := &PluginManager{}
err := pm.Init()
So(err, ShouldBeNil)
Convey(desc, fn)
})
}
+5 -4
View File
@@ -1,7 +1,6 @@
package plugins
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/bus"
@@ -15,10 +14,12 @@ import (
func TestPluginDashboards(t *testing.T) {
Convey("When asking plugin dashboard info", t, func() {
setting.Cfg = ini.Empty()
sec, _ := setting.Cfg.NewSection("plugin.test-app")
setting.Raw = ini.Empty()
sec, _ := setting.Raw.NewSection("plugin.test-app")
sec.NewKey("path", "../../tests/test-app")
err := initPlugins(context.Background())
pm := &PluginManager{}
err := pm.Init()
So(err, ShouldBeNil)
+24 -28
View File
@@ -1,8 +1,6 @@
package plugins
import (
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
@@ -11,10 +9,8 @@ func init() {
bus.AddEventListener(handlePluginStateChanged)
}
func updateAppDashboards() {
time.Sleep(time.Second * 5)
plog.Debug("Looking for App Dashboard Updates")
func (pm *PluginManager) updateAppDashboards() {
pm.log.Debug("Looking for App Dashboard Updates")
query := m.GetPluginSettingsQuery{OrgId: 0}
@@ -38,23 +34,24 @@ func updateAppDashboards() {
}
func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) error {
if dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path); err != nil {
dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path)
if err != nil {
return err
} else {
plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
updateCmd := ImportDashboardCommand{
OrgId: orgId,
PluginId: pluginDashInfo.PluginId,
Overwrite: true,
Dashboard: dash.Data,
User: &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN},
Path: pluginDashInfo.Path,
}
if err := bus.Dispatch(&updateCmd); err != nil {
return err
}
}
plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
updateCmd := ImportDashboardCommand{
OrgId: orgId,
PluginId: pluginDashInfo.PluginId,
Overwrite: true,
Dashboard: dash.Data,
User: &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN},
Path: pluginDashInfo.Path,
}
if err := bus.Dispatch(&updateCmd); err != nil {
return err
}
return nil
}
@@ -122,15 +119,14 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
if err := bus.Dispatch(&query); err != nil {
return err
} else {
for _, dash := range query.Result {
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
}
for _, dash := range query.Result {
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
if err := bus.Dispatch(&deleteCmd); err != nil {
return err
}
if err := bus.Dispatch(&deleteCmd); err != nil {
return err
}
}
}
@@ -74,7 +74,7 @@ func TestMappingRowValue(t *testing.T) {
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
haveBool, ok := boolRowValue.(bool)
if !ok || haveBool != true {
if !ok || !haveBool {
t.Fatalf("Expected true, was %v", haveBool)
}
+1 -1
View File
@@ -76,7 +76,7 @@ func composeBinaryName(executable, os, arch string) string {
return fmt.Sprintf("%s_%s_%s%s", executable, os, strings.ToLower(arch), extension)
}
func (p *DataSourcePlugin) initBackendPlugin(ctx context.Context, log log.Logger) error {
func (p *DataSourcePlugin) startBackendPlugin(ctx context.Context, log log.Logger) error {
p.log = log.New("plugin-id", p.Id)
err := p.spawnSubProcess()
+1 -1
View File
@@ -69,7 +69,7 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error {
for _, include := range pb.Includes {
if include.Role == "" {
include.Role = m.RoleType(m.ROLE_VIEWER)
include.Role = m.ROLE_VIEWER
}
}
+51 -39
View File
@@ -11,8 +11,10 @@ import (
"path/filepath"
"reflect"
"strings"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -39,30 +41,12 @@ type PluginManager struct {
log log.Logger
}
func NewPluginManager(ctx context.Context) (*PluginManager, error) {
err := initPlugins(ctx)
if err != nil {
return nil, err
}
return &PluginManager{
log: log.New("plugins"),
}, nil
func init() {
registry.RegisterService(&PluginManager{})
}
func (p *PluginManager) Run(ctx context.Context) error {
<-ctx.Done()
for _, p := range DataSources {
p.Kill()
}
p.log.Info("Stopped Plugins", "error", ctx.Err())
return ctx.Err()
}
func initPlugins(ctx context.Context) error {
func (pm *PluginManager) Init() error {
pm.log = log.New("plugins")
plog = log.New("plugins")
DataSources = map[string]*DataSourcePlugin{}
@@ -76,7 +60,7 @@ func initPlugins(ctx context.Context) error {
"app": AppPlugin{},
}
plog.Info("Starting plugin search")
pm.log.Info("Starting plugin search")
scan(path.Join(setting.StaticRootPath, "app/plugins"))
// check if plugins dir exists
@@ -99,13 +83,6 @@ func initPlugins(ctx context.Context) error {
}
for _, ds := range DataSources {
if ds.Backend {
err := ds.initBackendPlugin(ctx, plog)
if err != nil {
plog.Error("Failed to init plugin.", "error", err, "plugin", ds.Id)
}
}
ds.initFrontendPlugin()
}
@@ -113,14 +90,49 @@ func initPlugins(ctx context.Context) error {
app.initApp()
}
go StartPluginUpdateChecker()
go updateAppDashboards()
return nil
}
func (pm *PluginManager) startBackendPlugins(ctx context.Context) error {
for _, ds := range DataSources {
if ds.Backend {
if err := ds.startBackendPlugin(ctx, plog); err != nil {
pm.log.Error("Failed to init plugin.", "error", err, "plugin", ds.Id)
}
}
}
return nil
}
func (pm *PluginManager) Run(ctx context.Context) error {
pm.startBackendPlugins(ctx)
pm.updateAppDashboards()
pm.checkForUpdates()
ticker := time.NewTicker(time.Minute * 10)
run := true
for run {
select {
case <-ticker.C:
pm.checkForUpdates()
case <-ctx.Done():
run = false
break
}
}
// kil backend plugins
for _, p := range DataSources {
p.Kill()
}
return ctx.Err()
}
func checkPluginPaths() error {
for _, section := range setting.Cfg.Sections() {
for _, section := range setting.Raw.Sections() {
if strings.HasPrefix(section.Name(), "plugin.") {
path := section.Key("path").String()
if path != "" {
@@ -193,11 +205,11 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
}
var loader PluginLoader
if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
pluginGoType, exists := PluginTypes[pluginCommon.Type]
if !exists {
return errors.New("Unknown plugin type " + pluginCommon.Type)
} else {
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
}
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
reader.Seek(0, 0)
return loader.Load(jsonParser, currentDir)
@@ -218,9 +230,9 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
return make([]byte, 0), nil
}
if data, err := ioutil.ReadFile(path); err != nil {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
} else {
return data, nil
}
return data, nil
}
+10 -7
View File
@@ -1,7 +1,6 @@
package plugins
import (
"context"
"path/filepath"
"testing"
@@ -12,10 +11,12 @@ import (
func TestPluginScans(t *testing.T) {
Convey("When scaning for plugins", t, func() {
Convey("When scanning for plugins", t, func() {
setting.StaticRootPath, _ = filepath.Abs("../../public/")
setting.Cfg = ini.Empty()
err := initPlugins(context.Background())
setting.Raw = ini.Empty()
pm := &PluginManager{}
err := pm.Init()
So(err, ShouldBeNil)
So(len(DataSources), ShouldBeGreaterThan, 1)
@@ -27,10 +28,12 @@ func TestPluginScans(t *testing.T) {
})
Convey("When reading app plugin definition", t, func() {
setting.Cfg = ini.Empty()
sec, _ := setting.Cfg.NewSection("plugin.nginx-app")
setting.Raw = ini.Empty()
sec, _ := setting.Raw.NewSection("plugin.nginx-app")
sec.NewKey("path", "../../tests/test-app")
err := initPlugins(context.Background())
pm := &PluginManager{}
err := pm.Init()
So(err, ShouldBeNil)
So(len(Apps), ShouldBeGreaterThan, 0)
+1 -1
View File
@@ -37,7 +37,7 @@ func GetPluginSettings(orgId int64) (map[string]*m.PluginSettingInfoDTO, error)
// if it's included in app check app settings
if pluginDef.IncludedInAppId != "" {
// app componets are by default disabled
// app components are by default disabled
opt.Enabled = false
if appSettings, ok := pluginMap[pluginDef.IncludedInAppId]; ok {
+7 -20
View File
@@ -13,7 +13,7 @@ import (
)
var (
httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
httpClient = http.Client{Timeout: 10 * time.Second}
)
type GrafanaNetPlugin struct {
@@ -26,23 +26,6 @@ type GithubLatest struct {
Testing string `json:"testing"`
}
func StartPluginUpdateChecker() {
if !setting.CheckForUpdates {
return
}
// do one check directly
go checkForUpdates()
ticker := time.NewTicker(time.Minute * 10)
for {
select {
case <-ticker.C:
checkForUpdates()
}
}
}
func getAllExternalPluginSlugs() string {
var result []string
for _, plug := range Plugins {
@@ -56,8 +39,12 @@ func getAllExternalPluginSlugs() string {
return strings.Join(result, ",")
}
func checkForUpdates() {
log.Trace("Checking for updates")
func (pm *PluginManager) checkForUpdates() {
if !setting.CheckForUpdates {
return
}
pm.log.Debug("Checking for updates")
pluginSlugs := getAllExternalPluginSlugs()
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
+33
View File
@@ -0,0 +1,33 @@
package registry
import (
"context"
)
var services = []Service{}
func RegisterService(srv Service) {
services = append(services, srv)
}
func GetServices() []Service {
return services
}
type Service interface {
Init() error
}
// Useful for alerting service
type CanBeDisabled interface {
IsDisabled() bool
}
type BackgroundService interface {
Run(ctx context.Context) error
}
func IsDisabled(srv Service) bool {
canBeDisabled, ok := srv.(CanBeDisabled)
return ok && canBeDisabled.IsDisabled()
}
+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

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