Browse Source

API: Restrict anonymous user information access (#18422)

Existing /api/alert-notifications now requires at least editor access.
Existing /api/alert-notifiers now requires at least editor access.
New /api/alert-notifications/lookup returns less information than
/api/alert-notifications and can be access by any authenticated user.
Existing /api/org/users now requires org admin role.
New /api/org/users/lookup returns less information than
/api/org/users and can be access by users that are org admins,
admin in any folder or admin of any team.
UserPicker component now uses /api/org/users/lookup instead
of /api/org/users.

Fixes #17318
Marcus Efraimsson 6 years ago
parent
commit
8fd153edb7

+ 42 - 0
docs/sources/http_api/alerting_notification_channels.md

@@ -63,6 +63,48 @@ Content-Type: application/json
 
 ```
 
+## Get all notification channels (lookup)
+
+Returns all notification channels, but with less detailed information.
+Accessible by any authenticated user and is mainly used by providing
+alert notification channels in Grafana UI when configuring alert rule.
+
+`GET /api/alert-notifications/lookup`
+
+**Example Request**:
+
+```http
+GET /api/alert-notifications/lookup HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "id": 1,
+    "uid": "000000001",
+    "name": "Test",
+    "type": "email",
+    "isDefault": false
+  },
+  {
+    "id": 2,
+    "uid": "000000002",
+    "name": "Slack",
+    "type": "slack",
+    "isDefault": false
+  }
+]
+
+```
+
 ## Get notification channel by uid
 
 `GET /api/alert-notifications/uid/:uid`

+ 44 - 5
docs/sources/http_api/org.md

@@ -47,6 +47,9 @@ Content-Type: application/json
 
 `GET /api/org/users`
 
+Returns all org users within the current organization.
+Accessible to users with org admin role.
+
 **Example Request**:
 
 ```http
@@ -64,11 +67,47 @@ Content-Type: application/json
 
 [
   {
-    "orgId":1,
-    "userId":1,
-    "email":"admin@mygraf.com",
-    "login":"admin",
-    "role":"Admin"
+    "orgId": 1,
+    "userId": 1,
+    "email": "admin@localhost",
+    "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56",
+    "login": "admin",
+    "role": "Admin",
+    "lastSeenAt": "2019-08-09T11:02:49+02:00",
+    "lastSeenAtAge": "< 1m"
+  }
+]
+```
+
+### Get all users within the current organization (lookup)
+
+`GET /api/org/users/lookup`
+
+Returns all org users within the current organization, but with less detailed information.
+Accessible to users with org admin role, admin in any folder or admin of any team.
+Mainly used by Grafana UI for providing list of users when adding team members and
+when editing folder/dashboard permissions.
+
+**Example Request**:
+
+```http
+GET /api/org/users/lookup HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "userId": 1,
+    "login": "admin",
+    "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56"
   }
 ]
 ```

+ 65 - 41
pkg/api/alerting.go

@@ -6,15 +6,15 @@ import (
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/guardian"
 	"github.com/grafana/grafana/pkg/services/search"
 )
 
-func ValidateOrgAlert(c *m.ReqContext) {
+func ValidateOrgAlert(c *models.ReqContext) {
 	id := c.ParamsInt64(":alertId")
-	query := m.GetAlertByIdQuery{Id: id}
+	query := models.GetAlertByIdQuery{Id: id}
 
 	if err := bus.Dispatch(&query); err != nil {
 		c.JsonApiErr(404, "Alert not found", nil)
@@ -27,14 +27,14 @@ func ValidateOrgAlert(c *m.ReqContext) {
 	}
 }
 
-func GetAlertStatesForDashboard(c *m.ReqContext) Response {
+func GetAlertStatesForDashboard(c *models.ReqContext) Response {
 	dashboardID := c.QueryInt64("dashboardId")
 
 	if dashboardID == 0 {
 		return Error(400, "Missing query parameter dashboardId", nil)
 	}
 
-	query := m.GetAlertStatesForDashboardQuery{
+	query := models.GetAlertStatesForDashboardQuery{
 		OrgId:       c.OrgId,
 		DashboardId: c.QueryInt64("dashboardId"),
 	}
@@ -47,7 +47,7 @@ func GetAlertStatesForDashboard(c *m.ReqContext) Response {
 }
 
 // GET /api/alerts
-func GetAlerts(c *m.ReqContext) Response {
+func GetAlerts(c *models.ReqContext) Response {
 	dashboardQuery := c.Query("dashboardQuery")
 	dashboardTags := c.QueryStrings("dashboardTag")
 	stringDashboardIDs := c.QueryStrings("dashboardId")
@@ -79,7 +79,7 @@ func GetAlerts(c *m.ReqContext) Response {
 			DashboardIds: dashboardIDs,
 			Type:         string(search.DashHitDB),
 			FolderIds:    folderIDs,
-			Permission:   m.PERMISSION_VIEW,
+			Permission:   models.PERMISSION_VIEW,
 		}
 
 		err := bus.Dispatch(&searchQuery)
@@ -95,11 +95,11 @@ func GetAlerts(c *m.ReqContext) Response {
 
 		// if we didn't find any dashboards, return empty result
 		if len(dashboardIDs) == 0 {
-			return JSON(200, []*m.AlertListItemDTO{})
+			return JSON(200, []*models.AlertListItemDTO{})
 		}
 	}
 
-	query := m.GetAlertsQuery{
+	query := models.GetAlertsQuery{
 		OrgId:        c.OrgId,
 		DashboardIDs: dashboardIDs,
 		PanelId:      c.QueryInt64("panelId"),
@@ -118,14 +118,14 @@ func GetAlerts(c *m.ReqContext) Response {
 	}
 
 	for _, alert := range query.Result {
-		alert.Url = m.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
+		alert.Url = models.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
 	}
 
 	return JSON(200, query.Result)
 }
 
 // POST /api/alerts/test
-func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
+func AlertTest(c *models.ReqContext, dto dtos.AlertTestCommand) Response {
 	if _, idErr := dto.Dashboard.Get("id").Int64(); idErr != nil {
 		return Error(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil)
 	}
@@ -141,7 +141,7 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
 		if validationErr, ok := err.(alerting.ValidationError); ok {
 			return Error(422, validationErr.Error(), nil)
 		}
-		if err == m.ErrDataSourceAccessDenied {
+		if err == models.ErrDataSourceAccessDenied {
 			return Error(403, "Access denied to datasource", err)
 		}
 		return Error(500, "Failed to test rule", err)
@@ -171,9 +171,9 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
 }
 
 // GET /api/alerts/:id
-func GetAlert(c *m.ReqContext) Response {
+func GetAlert(c *models.ReqContext) Response {
 	id := c.ParamsInt64(":alertId")
-	query := m.GetAlertByIdQuery{Id: id}
+	query := models.GetAlertByIdQuery{Id: id}
 
 	if err := bus.Dispatch(&query); err != nil {
 		return Error(500, "List alerts failed", err)
@@ -182,28 +182,52 @@ func GetAlert(c *m.ReqContext) Response {
 	return JSON(200, &query.Result)
 }
 
-func GetAlertNotifiers(c *m.ReqContext) Response {
+func GetAlertNotifiers(c *models.ReqContext) Response {
 	return JSON(200, alerting.GetNotifiers())
 }
 
-func GetAlertNotifications(c *m.ReqContext) Response {
-	query := &m.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
+func GetAlertNotificationLookup(c *models.ReqContext) Response {
+	alertNotifications, err := getAlertNotificationsInternal(c)
+	if err != nil {
+		return Error(500, "Failed to get alert notifications", err)
+	}
 
-	if err := bus.Dispatch(query); err != nil {
+	result := make([]*dtos.AlertNotificationLookup, 0)
+
+	for _, notification := range alertNotifications {
+		result = append(result, dtos.NewAlertNotificationLookup(notification))
+	}
+
+	return JSON(200, result)
+}
+
+func GetAlertNotifications(c *models.ReqContext) Response {
+	alertNotifications, err := getAlertNotificationsInternal(c)
+	if err != nil {
 		return Error(500, "Failed to get alert notifications", err)
 	}
 
 	result := make([]*dtos.AlertNotification, 0)
 
-	for _, notification := range query.Result {
+	for _, notification := range alertNotifications {
 		result = append(result, dtos.NewAlertNotification(notification))
 	}
 
 	return JSON(200, result)
 }
 
-func GetAlertNotificationByID(c *m.ReqContext) Response {
-	query := &m.GetAlertNotificationsQuery{
+func getAlertNotificationsInternal(c *models.ReqContext) ([]*models.AlertNotification, error) {
+	query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
+
+	if err := bus.Dispatch(query); err != nil {
+		return nil, err
+	}
+
+	return query.Result, nil
+}
+
+func GetAlertNotificationByID(c *models.ReqContext) Response {
+	query := &models.GetAlertNotificationsQuery{
 		OrgId: c.OrgId,
 		Id:    c.ParamsInt64("notificationId"),
 	}
@@ -223,8 +247,8 @@ func GetAlertNotificationByID(c *m.ReqContext) Response {
 	return JSON(200, dtos.NewAlertNotification(query.Result))
 }
 
-func GetAlertNotificationByUID(c *m.ReqContext) Response {
-	query := &m.GetAlertNotificationsWithUidQuery{
+func GetAlertNotificationByUID(c *models.ReqContext) Response {
+	query := &models.GetAlertNotificationsWithUidQuery{
 		OrgId: c.OrgId,
 		Uid:   c.Params("uid"),
 	}
@@ -244,7 +268,7 @@ func GetAlertNotificationByUID(c *m.ReqContext) Response {
 	return JSON(200, dtos.NewAlertNotification(query.Result))
 }
 
-func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response {
+func CreateAlertNotification(c *models.ReqContext, cmd models.CreateAlertNotificationCommand) Response {
 	cmd.OrgId = c.OrgId
 
 	if err := bus.Dispatch(&cmd); err != nil {
@@ -254,7 +278,7 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma
 	return JSON(200, dtos.NewAlertNotification(cmd.Result))
 }
 
-func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response {
+func UpdateAlertNotification(c *models.ReqContext, cmd models.UpdateAlertNotificationCommand) Response {
 	cmd.OrgId = c.OrgId
 
 	if err := bus.Dispatch(&cmd); err != nil {
@@ -268,7 +292,7 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma
 	return JSON(200, dtos.NewAlertNotification(cmd.Result))
 }
 
-func UpdateAlertNotificationByUID(c *m.ReqContext, cmd m.UpdateAlertNotificationWithUidCommand) Response {
+func UpdateAlertNotificationByUID(c *models.ReqContext, cmd models.UpdateAlertNotificationWithUidCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.Uid = c.Params("uid")
 
@@ -283,8 +307,8 @@ func UpdateAlertNotificationByUID(c *m.ReqContext, cmd m.UpdateAlertNotification
 	return JSON(200, dtos.NewAlertNotification(cmd.Result))
 }
 
-func DeleteAlertNotification(c *m.ReqContext) Response {
-	cmd := m.DeleteAlertNotificationCommand{
+func DeleteAlertNotification(c *models.ReqContext) Response {
+	cmd := models.DeleteAlertNotificationCommand{
 		OrgId: c.OrgId,
 		Id:    c.ParamsInt64("notificationId"),
 	}
@@ -296,8 +320,8 @@ func DeleteAlertNotification(c *m.ReqContext) Response {
 	return Success("Notification deleted")
 }
 
-func DeleteAlertNotificationByUID(c *m.ReqContext) Response {
-	cmd := m.DeleteAlertNotificationWithUidCommand{
+func DeleteAlertNotificationByUID(c *models.ReqContext) Response {
+	cmd := models.DeleteAlertNotificationWithUidCommand{
 		OrgId: c.OrgId,
 		Uid:   c.Params("uid"),
 	}
@@ -310,7 +334,7 @@ func DeleteAlertNotificationByUID(c *m.ReqContext) Response {
 }
 
 //POST /api/alert-notifications/test
-func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Response {
+func NotificationTest(c *models.ReqContext, dto dtos.NotificationTestCommand) Response {
 	cmd := &alerting.NotificationTestCommand{
 		Name:     dto.Name,
 		Type:     dto.Type,
@@ -318,7 +342,7 @@ func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Respons
 	}
 
 	if err := bus.Dispatch(cmd); err != nil {
-		if err == m.ErrSmtpNotEnabled {
+		if err == models.ErrSmtpNotEnabled {
 			return Error(412, err.Error(), err)
 		}
 		return Error(500, "Failed to send alert notifications", err)
@@ -328,10 +352,10 @@ func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Respons
 }
 
 //POST /api/alerts/:alertId/pause
-func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
+func PauseAlert(c *models.ReqContext, dto dtos.PauseAlertCommand) Response {
 	alertID := c.ParamsInt64("alertId")
 
-	query := m.GetAlertByIdQuery{Id: alertID}
+	query := models.GetAlertByIdQuery{Id: alertID}
 
 	if err := bus.Dispatch(&query); err != nil {
 		return Error(500, "Get Alert failed", err)
@@ -346,7 +370,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
 		return Error(403, "Access denied to this dashboard and alert", nil)
 	}
 
-	cmd := m.PauseAlertCommand{
+	cmd := models.PauseAlertCommand{
 		OrgId:    c.OrgId,
 		AlertIds: []int64{alertID},
 		Paused:   dto.Paused,
@@ -356,10 +380,10 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
 		return Error(500, "", err)
 	}
 
-	var response m.AlertStateType = m.AlertStateUnknown
+	var response models.AlertStateType = models.AlertStateUnknown
 	pausedState := "un-paused"
 	if cmd.Paused {
-		response = m.AlertStatePaused
+		response = models.AlertStatePaused
 		pausedState = "paused"
 	}
 
@@ -373,8 +397,8 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
 }
 
 //POST /api/admin/pause-all-alerts
-func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
-	updateCmd := m.PauseAllAlertCommand{
+func PauseAllAlerts(c *models.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
+	updateCmd := models.PauseAllAlertCommand{
 		Paused: dto.Paused,
 	}
 
@@ -382,10 +406,10 @@ func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
 		return Error(500, "Failed to pause alerts", err)
 	}
 
-	var response m.AlertStateType = m.AlertStatePending
+	var response models.AlertStateType = models.AlertStatePending
 	pausedState := "un paused"
 	if updateCmd.Paused {
-		response = m.AlertStatePaused
+		response = models.AlertStatePaused
 		pausedState = "paused"
 	}
 

+ 24 - 24
pkg/api/alerting_test.go

@@ -5,7 +5,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/search"
 
 	. "github.com/smartystreets/goconvey/convey"
@@ -14,24 +14,24 @@ import (
 func TestAlertingApiEndpoint(t *testing.T) {
 	Convey("Given an alert in a dashboard with an acl", t, func() {
 
-		singleAlert := &m.Alert{Id: 1, DashboardId: 1, Name: "singlealert"}
+		singleAlert := &models.Alert{Id: 1, DashboardId: 1, Name: "singlealert"}
 
-		bus.AddHandler("test", func(query *m.GetAlertByIdQuery) error {
+		bus.AddHandler("test", func(query *models.GetAlertByIdQuery) error {
 			query.Result = singleAlert
 			return nil
 		})
 
-		viewerRole := m.ROLE_VIEWER
-		editorRole := m.ROLE_EDITOR
+		viewerRole := models.ROLE_VIEWER
+		editorRole := models.ROLE_EDITOR
 
-		aclMockResp := []*m.DashboardAclInfoDTO{}
-		bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+		aclMockResp := []*models.DashboardAclInfoDTO{}
+		bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error {
 			query.Result = aclMockResp
 			return nil
 		})
 
-		bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
-			query.Result = []*m.TeamDTO{}
+		bus.AddHandler("test", func(query *models.GetTeamsByUserQuery) error {
+			query.Result = []*models.TeamDTO{}
 			return nil
 		})
 
@@ -41,7 +41,7 @@ func TestAlertingApiEndpoint(t *testing.T) {
 					AlertId: 1,
 					Paused:  true,
 				}
-				postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
+				postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", models.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
 					CallPauseAlert(sc)
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
@@ -49,9 +49,9 @@ func TestAlertingApiEndpoint(t *testing.T) {
 		})
 
 		Convey("When user is editor and dashboard has default ACL", func() {
-			aclMockResp = []*m.DashboardAclInfoDTO{
-				{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
-				{Role: &editorRole, Permission: m.PERMISSION_EDIT},
+			aclMockResp = []*models.DashboardAclInfoDTO{
+				{Role: &viewerRole, Permission: models.PERMISSION_VIEW},
+				{Role: &editorRole, Permission: models.PERMISSION_EDIT},
 			}
 
 			Convey("Should be able to pause the alert", func() {
@@ -59,22 +59,22 @@ func TestAlertingApiEndpoint(t *testing.T) {
 					AlertId: 1,
 					Paused:  true,
 				}
-				postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
+				postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", models.ROLE_EDITOR, cmd, func(sc *scenarioContext) {
 					CallPauseAlert(sc)
 					So(sc.resp.Code, ShouldEqual, 200)
 				})
 			})
 		})
 
-		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) {
+		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts", models.ROLE_EDITOR, func(sc *scenarioContext) {
 			var searchQuery *search.Query
 			bus.AddHandler("test", func(query *search.Query) error {
 				searchQuery = query
 				return nil
 			})
 
-			var getAlertsQuery *m.GetAlertsQuery
-			bus.AddHandler("test", func(query *m.GetAlertsQuery) error {
+			var getAlertsQuery *models.GetAlertsQuery
+			bus.AddHandler("test", func(query *models.GetAlertsQuery) error {
 				getAlertsQuery = query
 				return nil
 			})
@@ -86,7 +86,7 @@ func TestAlertingApiEndpoint(t *testing.T) {
 			So(getAlertsQuery, ShouldNotBeNil)
 		})
 
-		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) {
+		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery", "/api/alerts", models.ROLE_EDITOR, func(sc *scenarioContext) {
 			var searchQuery *search.Query
 			bus.AddHandler("test", func(query *search.Query) error {
 				searchQuery = query
@@ -97,8 +97,8 @@ func TestAlertingApiEndpoint(t *testing.T) {
 				return nil
 			})
 
-			var getAlertsQuery *m.GetAlertsQuery
-			bus.AddHandler("test", func(query *m.GetAlertsQuery) error {
+			var getAlertsQuery *models.GetAlertsQuery
+			bus.AddHandler("test", func(query *models.GetAlertsQuery) error {
 				getAlertsQuery = query
 				return nil
 			})
@@ -120,7 +120,7 @@ func TestAlertingApiEndpoint(t *testing.T) {
 			So(getAlertsQuery.Query, ShouldEqual, "alertQuery")
 		})
 
-		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alert-notifications/1", "/alert-notifications/:notificationId", m.ROLE_ADMIN, func(sc *scenarioContext) {
+		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alert-notifications/1", "/alert-notifications/:notificationId", models.ROLE_ADMIN, func(sc *scenarioContext) {
 			sc.handlerFunc = GetAlertNotificationByID
 			sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
 			So(sc.resp.Code, ShouldEqual, 404)
@@ -129,19 +129,19 @@ func TestAlertingApiEndpoint(t *testing.T) {
 }
 
 func CallPauseAlert(sc *scenarioContext) {
-	bus.AddHandler("test", func(cmd *m.PauseAlertCommand) error {
+	bus.AddHandler("test", func(cmd *models.PauseAlertCommand) error {
 		return nil
 	})
 
 	sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
 }
 
-func postAlertScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PauseAlertCommand, fn scenarioFunc) {
+func postAlertScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.PauseAlertCommand, fn scenarioFunc) {
 	Convey(desc+" "+url, func() {
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *models.ReqContext) Response {
 			sc.context = c
 			sc.context.UserId = TestUserID
 			sc.context.OrgId = TestOrgID

+ 9 - 3
pkg/api/api.go

@@ -183,6 +183,7 @@ func (hs *HTTPServer) registerRoutes() {
 		apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
 			orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
 			orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
+			orgRoute.Get("/users", Wrap(GetOrgUsersForCurrentOrg))
 			orgRoute.Post("/users", quota("user"), bind(models.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
 			orgRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
 			orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
@@ -199,7 +200,7 @@ func (hs *HTTPServer) registerRoutes() {
 
 		// current org without requirement of user to be org admin
 		apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
-			orgRoute.Get("/users", Wrap(GetOrgUsersForCurrentOrg))
+			orgRoute.Get("/users/lookup", Wrap(GetOrgUsersForCurrentOrgLookup))
 		})
 
 		// create new org
@@ -343,10 +344,10 @@ func (hs *HTTPServer) registerRoutes() {
 			alertsRoute.Get("/states-for-dashboard", Wrap(GetAlertStatesForDashboard))
 		})
 
-		apiRoute.Get("/alert-notifications", Wrap(GetAlertNotifications))
-		apiRoute.Get("/alert-notifiers", Wrap(GetAlertNotifiers))
+		apiRoute.Get("/alert-notifiers", reqEditorRole, Wrap(GetAlertNotifiers))
 
 		apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
+			alertNotifications.Get("/", Wrap(GetAlertNotifications))
 			alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), Wrap(NotificationTest))
 			alertNotifications.Post("/", bind(models.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
 			alertNotifications.Put("/:notificationId", bind(models.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
@@ -357,6 +358,11 @@ func (hs *HTTPServer) registerRoutes() {
 			alertNotifications.Delete("/uid/:uid", Wrap(DeleteAlertNotificationByUID))
 		}, reqEditorRole)
 
+		// alert notifications without requirement of user to be org editor
+		apiRoute.Group("/alert-notifications", func(orgRoute routing.RouteRegister) {
+			orgRoute.Get("/lookup", Wrap(GetAlertNotificationLookup))
+		})
+
 		apiRoute.Get("/annotations", Wrap(GetAnnotations))
 		apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), Wrap(DeleteAnnotations))
 

+ 18 - 0
pkg/api/dtos/alerting.go

@@ -77,6 +77,24 @@ type AlertNotification struct {
 	Settings              *simplejson.Json `json:"settings"`
 }
 
+func NewAlertNotificationLookup(notification *models.AlertNotification) *AlertNotificationLookup {
+	return &AlertNotificationLookup{
+		Id:        notification.Id,
+		Uid:       notification.Uid,
+		Name:      notification.Name,
+		Type:      notification.Type,
+		IsDefault: notification.IsDefault,
+	}
+}
+
+type AlertNotificationLookup struct {
+	Id        int64  `json:"id"`
+	Uid       string `json:"uid"`
+	Name      string `json:"name"`
+	Type      string `json:"type"`
+	IsDefault bool   `json:"isDefault"`
+}
+
 type AlertTestCommand struct {
 	Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
 	PanelId   int64            `json:"panelId" binding:"Required"`

+ 6 - 0
pkg/api/dtos/user.go

@@ -50,3 +50,9 @@ type ResetUserPasswordForm struct {
 	NewPassword     string `json:"newPassword"`
 	ConfirmPassword string `json:"confirmPassword"`
 }
+
+type UserLookupDTO struct {
+	UserID    int64  `json:"userId"`
+	Login     string `json:"login"`
+	AvatarURL string `json:"avatarUrl"`
+}

+ 85 - 24
pkg/api/org_users.go

@@ -3,27 +3,27 @@ package api
 import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 )
 
 // POST /api/org/users
-func AddOrgUserToCurrentOrg(c *m.ReqContext, cmd m.AddOrgUserCommand) Response {
+func AddOrgUserToCurrentOrg(c *models.ReqContext, cmd models.AddOrgUserCommand) Response {
 	cmd.OrgId = c.OrgId
 	return addOrgUserHelper(cmd)
 }
 
 // POST /api/orgs/:orgId/users
-func AddOrgUser(c *m.ReqContext, cmd m.AddOrgUserCommand) Response {
+func AddOrgUser(c *models.ReqContext, cmd models.AddOrgUserCommand) Response {
 	cmd.OrgId = c.ParamsInt64(":orgId")
 	return addOrgUserHelper(cmd)
 }
 
-func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
+func addOrgUserHelper(cmd models.AddOrgUserCommand) Response {
 	if !cmd.Role.IsValid() {
 		return Error(400, "Invalid role specified", nil)
 	}
 
-	userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
+	userQuery := models.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
 	err := bus.Dispatch(&userQuery)
 	if err != nil {
 		return Error(404, "User not found", nil)
@@ -34,7 +34,7 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
 	cmd.UserId = userToAdd.Id
 
 	if err := bus.Dispatch(&cmd); err != nil {
-		if err == m.ErrOrgUserAlreadyAdded {
+		if err == models.ErrOrgUserAlreadyAdded {
 			return Error(409, "User is already member of this organization", nil)
 		}
 		return Error(500, "Could not add user to organization", err)
@@ -44,54 +44,115 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
 }
 
 // GET /api/org/users
-func GetOrgUsersForCurrentOrg(c *m.ReqContext) Response {
-	return getOrgUsersHelper(c.OrgId, c.Query("query"), c.QueryInt("limit"))
+func GetOrgUsersForCurrentOrg(c *models.ReqContext) Response {
+	result, err := getOrgUsersHelper(c.OrgId, c.Query("query"), c.QueryInt("limit"))
+	if err != nil {
+		return Error(500, "Failed to get users for current organization", err)
+	}
+
+	return JSON(200, result)
+}
+
+// GET /api/org/users/lookup
+func GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) Response {
+	isAdmin, err := isOrgAdminFolderAdminOrTeamAdmin(c)
+	if err != nil {
+		return Error(500, "Failed to get users for current organization", err)
+	}
+
+	if !isAdmin {
+		return Error(403, "Permission denied", nil)
+	}
+
+	orgUsers, err := getOrgUsersHelper(c.OrgId, c.Query("query"), c.QueryInt("limit"))
+	if err != nil {
+		return Error(500, "Failed to get users for current organization", err)
+	}
+
+	result := make([]*dtos.UserLookupDTO, 0)
+
+	for _, u := range orgUsers {
+		result = append(result, &dtos.UserLookupDTO{
+			UserID:    u.UserId,
+			Login:     u.Login,
+			AvatarURL: u.AvatarUrl,
+		})
+	}
+
+	return JSON(200, result)
+}
+
+func isOrgAdminFolderAdminOrTeamAdmin(c *models.ReqContext) (bool, error) {
+	if c.OrgRole == models.ROLE_ADMIN {
+		return true, nil
+	}
+
+	hasAdminPermissionInFoldersQuery := models.HasAdminPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
+	if err := bus.Dispatch(&hasAdminPermissionInFoldersQuery); err != nil {
+		return false, err
+	}
+
+	if hasAdminPermissionInFoldersQuery.Result {
+		return true, nil
+	}
+
+	isAdminOfTeamsQuery := models.IsAdminOfTeamsQuery{SignedInUser: c.SignedInUser}
+	if err := bus.Dispatch(&isAdminOfTeamsQuery); err != nil {
+		return false, err
+	}
+
+	return isAdminOfTeamsQuery.Result, nil
 }
 
 // GET /api/orgs/:orgId/users
-func GetOrgUsers(c *m.ReqContext) Response {
-	return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
+func GetOrgUsers(c *models.ReqContext) Response {
+	result, err := getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
+	if err != nil {
+		return Error(500, "Failed to get users for organization", err)
+	}
+
+	return JSON(200, result)
 }
 
-func getOrgUsersHelper(orgID int64, query string, limit int) Response {
-	q := m.GetOrgUsersQuery{
+func getOrgUsersHelper(orgID int64, query string, limit int) ([]*models.OrgUserDTO, error) {
+	q := models.GetOrgUsersQuery{
 		OrgId: orgID,
 		Query: query,
 		Limit: limit,
 	}
 
 	if err := bus.Dispatch(&q); err != nil {
-		return Error(500, "Failed to get account user", err)
+		return nil, err
 	}
 
 	for _, user := range q.Result {
 		user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
 	}
 
-	return JSON(200, q.Result)
+	return q.Result, nil
 }
 
 // PATCH /api/org/users/:userId
-func UpdateOrgUserForCurrentOrg(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response {
+func UpdateOrgUserForCurrentOrg(c *models.ReqContext, cmd models.UpdateOrgUserCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.UserId = c.ParamsInt64(":userId")
 	return updateOrgUserHelper(cmd)
 }
 
 // PATCH /api/orgs/:orgId/users/:userId
-func UpdateOrgUser(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response {
+func UpdateOrgUser(c *models.ReqContext, cmd models.UpdateOrgUserCommand) Response {
 	cmd.OrgId = c.ParamsInt64(":orgId")
 	cmd.UserId = c.ParamsInt64(":userId")
 	return updateOrgUserHelper(cmd)
 }
 
-func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
+func updateOrgUserHelper(cmd models.UpdateOrgUserCommand) Response {
 	if !cmd.Role.IsValid() {
 		return Error(400, "Invalid role specified", nil)
 	}
 
 	if err := bus.Dispatch(&cmd); err != nil {
-		if err == m.ErrLastOrgAdmin {
+		if err == models.ErrLastOrgAdmin {
 			return Error(400, "Cannot change role so that there is no organization admin left", nil)
 		}
 		return Error(500, "Failed update org user", err)
@@ -101,8 +162,8 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
 }
 
 // DELETE /api/org/users/:userId
-func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response {
-	return removeOrgUserHelper(&m.RemoveOrgUserCommand{
+func RemoveOrgUserForCurrentOrg(c *models.ReqContext) Response {
+	return removeOrgUserHelper(&models.RemoveOrgUserCommand{
 		UserId:                   c.ParamsInt64(":userId"),
 		OrgId:                    c.OrgId,
 		ShouldDeleteOrphanedUser: true,
@@ -110,16 +171,16 @@ func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response {
 }
 
 // DELETE /api/orgs/:orgId/users/:userId
-func RemoveOrgUser(c *m.ReqContext) Response {
-	return removeOrgUserHelper(&m.RemoveOrgUserCommand{
+func RemoveOrgUser(c *models.ReqContext) Response {
+	return removeOrgUserHelper(&models.RemoveOrgUserCommand{
 		UserId: c.ParamsInt64(":userId"),
 		OrgId:  c.ParamsInt64(":orgId"),
 	})
 }
 
-func removeOrgUserHelper(cmd *m.RemoveOrgUserCommand) Response {
+func removeOrgUserHelper(cmd *models.RemoveOrgUserCommand) Response {
 	if err := bus.Dispatch(cmd); err != nil {
-		if err == m.ErrLastOrgAdmin {
+		if err == models.ErrLastOrgAdmin {
 			return Error(400, "Cannot remove last organization admin", nil)
 		}
 		return Error(500, "Failed to remove user from organization", err)

+ 5 - 0
pkg/models/folders.go

@@ -98,3 +98,8 @@ type HasEditPermissionInFoldersQuery struct {
 	SignedInUser *SignedInUser
 	Result       bool
 }
+
+type HasAdminPermissionInFoldersQuery struct {
+	SignedInUser *SignedInUser
+	Result       bool
+}

+ 5 - 0
pkg/models/team.go

@@ -88,3 +88,8 @@ type SearchTeamQueryResult struct {
 	Page       int        `json:"page"`
 	PerPage    int        `json:"perPage"`
 }
+
+type IsAdminOfTeamsQuery struct {
+	SignedInUser *SignedInUser
+	Result       bool
+}

+ 86 - 61
pkg/services/sqlstore/dashboard.go

@@ -6,7 +6,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/infra/metrics"
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/search"
 	"github.com/grafana/grafana/pkg/util"
 )
@@ -25,17 +25,18 @@ func init() {
 	bus.AddHandler("sql", GetDashboardsBySlug)
 	bus.AddHandler("sql", ValidateDashboardBeforeSave)
 	bus.AddHandler("sql", HasEditPermissionInFolders)
+	bus.AddHandler("sql", HasAdminPermissionInFolders)
 }
 
 var generateNewUid func() string = util.GenerateShortUID
 
-func SaveDashboard(cmd *m.SaveDashboardCommand) error {
+func SaveDashboard(cmd *models.SaveDashboardCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		return saveDashboard(sess, cmd)
 	})
 }
 
-func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
+func saveDashboard(sess *DBSession, cmd *models.SaveDashboardCommand) error {
 	dash := cmd.GetDashboardModel()
 
 	userId := cmd.UserId
@@ -45,13 +46,13 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
 	}
 
 	if dash.Id > 0 {
-		var existing m.Dashboard
+		var existing models.Dashboard
 		dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
 		if err != nil {
 			return err
 		}
 		if !dashWithIdExists {
-			return m.ErrDashboardNotFound
+			return models.ErrDashboardNotFound
 		}
 
 		// check for is someone else has written in between
@@ -59,13 +60,13 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
 			if cmd.Overwrite {
 				dash.SetVersion(existing.Version)
 			} else {
-				return m.ErrDashboardVersionMismatch
+				return models.ErrDashboardVersionMismatch
 			}
 		}
 
 		// do not allow plugin dashboard updates without overwrite flag
 		if existing.PluginId != "" && !cmd.Overwrite {
-			return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
+			return models.UpdatePluginDashboardError{PluginId: existing.PluginId}
 		}
 	}
 
@@ -108,10 +109,10 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
 	}
 
 	if affectedRows == 0 {
-		return m.ErrDashboardNotFound
+		return models.ErrDashboardNotFound
 	}
 
-	dashVersion := &m.DashboardVersion{
+	dashVersion := &models.DashboardVersion{
 		DashboardId:   dash.Id,
 		ParentVersion: parentVersion,
 		RestoredFrom:  cmd.RestoredFrom,
@@ -126,7 +127,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
 	if affectedRows, err = sess.Insert(dashVersion); err != nil {
 		return err
 	} else if affectedRows == 0 {
-		return m.ErrDashboardNotFound
+		return models.ErrDashboardNotFound
 	}
 
 	// delete existing tags
@@ -154,7 +155,7 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
 	for i := 0; i < 3; i++ {
 		uid := generateNewUid()
 
-		exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.Dashboard{})
+		exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&models.Dashboard{})
 		if err != nil {
 			return "", err
 		}
@@ -164,17 +165,17 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
 		}
 	}
 
-	return "", m.ErrDashboardFailedGenerateUniqueUid
+	return "", models.ErrDashboardFailedGenerateUniqueUid
 }
 
-func GetDashboard(query *m.GetDashboardQuery) error {
-	dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid}
+func GetDashboard(query *models.GetDashboardQuery) error {
+	dashboard := models.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid}
 	has, err := x.Get(&dashboard)
 
 	if err != nil {
 		return err
 	} else if !has {
-		return m.ErrDashboardNotFound
+		return models.ErrDashboardNotFound
 	}
 
 	dashboard.SetId(dashboard.Id)
@@ -262,7 +263,7 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
 				Uid:         item.Uid,
 				Title:       item.Title,
 				Uri:         "db/" + item.Slug,
-				Url:         m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
+				Url:         models.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
 				Type:        getHitType(item),
 				FolderId:    item.FolderId,
 				FolderUid:   item.FolderUid,
@@ -271,7 +272,7 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
 			}
 
 			if item.FolderId > 0 {
-				hit.FolderUrl = m.GetFolderUrl(item.FolderUid, item.FolderSlug)
+				hit.FolderUrl = models.GetFolderUrl(item.FolderUid, item.FolderSlug)
 			}
 
 			query.Result = append(query.Result, hit)
@@ -283,7 +284,7 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
 	}
 }
 
-func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
+func GetDashboardTags(query *models.GetDashboardTagsQuery) error {
 	sql := `SELECT
 					  COUNT(*) as count,
 						term
@@ -293,20 +294,20 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
 					GROUP BY term
 					ORDER BY term`
 
-	query.Result = make([]*m.DashboardTagCloudItem, 0)
+	query.Result = make([]*models.DashboardTagCloudItem, 0)
 	sess := x.SQL(sql, query.OrgId)
 	err := sess.Find(&query.Result)
 	return err
 }
 
-func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
+func DeleteDashboard(cmd *models.DeleteDashboardCommand) error {
 	return inTransaction(func(sess *DBSession) error {
-		dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
+		dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
 		has, err := sess.Get(&dashboard)
 		if err != nil {
 			return err
 		} else if !has {
-			return m.ErrDashboardNotFound
+			return models.ErrDashboardNotFound
 		}
 
 		deletes := []string{
@@ -354,12 +355,12 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 	})
 }
 
-func GetDashboards(query *m.GetDashboardsQuery) error {
+func GetDashboards(query *models.GetDashboardsQuery) error {
 	if len(query.DashboardIds) == 0 {
-		return m.ErrCommandValidationFailed
+		return models.ErrCommandValidationFailed
 	}
 
-	var dashboards = make([]*m.Dashboard, 0)
+	var dashboards = make([]*models.Dashboard, 0)
 
 	err := x.In("id", query.DashboardIds).Find(&dashboards)
 	query.Result = dashboards
@@ -368,18 +369,18 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
 
 // GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
 // The function takes in a list of dashboard ids and the user id and role
-func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery) error {
+func GetDashboardPermissionsForUser(query *models.GetDashboardPermissionsForUserQuery) error {
 	if len(query.DashboardIds) == 0 {
-		return m.ErrCommandValidationFailed
+		return models.ErrCommandValidationFailed
 	}
 
-	if query.OrgRole == m.ROLE_ADMIN {
-		var permissions = make([]*m.DashboardPermissionForUser, 0)
+	if query.OrgRole == models.ROLE_ADMIN {
+		var permissions = make([]*models.DashboardPermissionForUser, 0)
 		for _, d := range query.DashboardIds {
-			permissions = append(permissions, &m.DashboardPermissionForUser{
+			permissions = append(permissions, &models.DashboardPermissionForUser{
 				DashboardId:    d,
-				Permission:     m.PERMISSION_ADMIN,
-				PermissionName: m.PERMISSION_ADMIN.String(),
+				Permission:     models.PERMISSION_ADMIN,
+				PermissionName: models.PERMISSION_ADMIN.String(),
 			})
 		}
 		query.Result = permissions
@@ -436,8 +437,8 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery
 	return err
 }
 
-func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
-	var dashboards = make([]*m.Dashboard, 0)
+func GetDashboardsByPluginId(query *models.GetDashboardsByPluginIdQuery) error {
+	var dashboards = make([]*models.Dashboard, 0)
 	whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + dialect.BooleanStr(false)
 
 	err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
@@ -449,7 +450,7 @@ type DashboardSlugDTO struct {
 	Slug string
 }
 
-func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
+func GetDashboardSlugById(query *models.GetDashboardSlugByIdQuery) error {
 	var rawSql = `SELECT slug from dashboard WHERE Id=?`
 	var slug = DashboardSlugDTO{}
 
@@ -458,15 +459,15 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
 	if err != nil {
 		return err
 	} else if !exists {
-		return m.ErrDashboardNotFound
+		return models.ErrDashboardNotFound
 	}
 
 	query.Result = slug.Slug
 	return nil
 }
 
-func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error {
-	var dashboards []*m.Dashboard
+func GetDashboardsBySlug(query *models.GetDashboardsBySlugQuery) error {
+	var dashboards []*models.Dashboard
 
 	if err := x.Where("org_id=? AND slug=?", query.OrgId, query.Slug).Find(&dashboards); err != nil {
 		return err
@@ -476,28 +477,28 @@ func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error {
 	return nil
 }
 
-func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
+func GetDashboardUIDById(query *models.GetDashboardRefByIdQuery) error {
 	var rawSql = `SELECT uid, slug from dashboard WHERE Id=?`
 
-	us := &m.DashboardRef{}
+	us := &models.DashboardRef{}
 
 	exists, err := x.SQL(rawSql, query.Id).Get(us)
 
 	if err != nil {
 		return err
 	} else if !exists {
-		return m.ErrDashboardNotFound
+		return models.ErrDashboardNotFound
 	}
 
 	query.Result = us
 	return nil
 }
 
-func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
+func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *models.ValidateDashboardBeforeSaveCommand) (err error) {
 	dash := cmd.Dashboard
 
 	dashWithIdExists := false
-	var existingById m.Dashboard
+	var existingById models.Dashboard
 
 	if dash.Id > 0 {
 		dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
@@ -506,7 +507,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 		}
 
 		if !dashWithIdExists {
-			return m.ErrDashboardNotFound
+			return models.ErrDashboardNotFound
 		}
 
 		if dash.Uid == "" {
@@ -515,7 +516,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 	}
 
 	dashWithUidExists := false
-	var existingByUid m.Dashboard
+	var existingByUid models.Dashboard
 
 	if dash.Uid != "" {
 		dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
@@ -525,14 +526,14 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 	}
 
 	if dash.FolderId > 0 {
-		var existingFolder m.Dashboard
+		var existingFolder models.Dashboard
 		folderExists, folderErr := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, dialect.BooleanStr(true)).Get(&existingFolder)
 		if folderErr != nil {
 			return folderErr
 		}
 
 		if !folderExists {
-			return m.ErrDashboardFolderNotFound
+			return models.ErrDashboardFolderNotFound
 		}
 	}
 
@@ -541,7 +542,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 	}
 
 	if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
-		return m.ErrDashboardWithSameUIDExists
+		return models.ErrDashboardWithSameUIDExists
 	}
 
 	existing := existingById
@@ -558,7 +559,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 
 	if (existing.IsFolder && !dash.IsFolder) ||
 		(!existing.IsFolder && dash.IsFolder) {
-		return m.ErrDashboardTypeMismatch
+		return models.ErrDashboardTypeMismatch
 	}
 
 	if !dash.IsFolder && dash.FolderId != existing.FolderId {
@@ -570,21 +571,21 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
 		if cmd.Overwrite {
 			dash.SetVersion(existing.Version)
 		} else {
-			return m.ErrDashboardVersionMismatch
+			return models.ErrDashboardVersionMismatch
 		}
 	}
 
 	// do not allow plugin dashboard updates without overwrite flag
 	if existing.PluginId != "" && !cmd.Overwrite {
-		return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
+		return models.UpdatePluginDashboardError{PluginId: existing.PluginId}
 	}
 
 	return nil
 }
 
-func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) error {
+func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *models.ValidateDashboardBeforeSaveCommand) error {
 	dash := cmd.Dashboard
-	var existing m.Dashboard
+	var existing models.Dashboard
 
 	exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existing)
 	if err != nil {
@@ -593,11 +594,11 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
 
 	if exists && dash.Id != existing.Id {
 		if existing.IsFolder && !dash.IsFolder {
-			return m.ErrDashboardWithSameNameAsFolder
+			return models.ErrDashboardWithSameNameAsFolder
 		}
 
 		if !existing.IsFolder && dash.IsFolder {
-			return m.ErrDashboardFolderWithSameNameAsDashboard
+			return models.ErrDashboardFolderWithSameNameAsDashboard
 		}
 
 		if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) {
@@ -609,15 +610,15 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
 			dash.SetUid(existing.Uid)
 			dash.SetVersion(existing.Version)
 		} else {
-			return m.ErrDashboardWithSameNameInFolderExists
+			return models.ErrDashboardWithSameNameInFolderExists
 		}
 	}
 
 	return nil
 }
 
-func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
-	cmd.Result = &m.ValidateDashboardBeforeSaveResult{}
+func ValidateDashboardBeforeSave(cmd *models.ValidateDashboardBeforeSaveCommand) (err error) {
+	cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
 	return inTransaction(func(sess *DBSession) error {
 		if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
 			return err
@@ -631,15 +632,39 @@ func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err
 	})
 }
 
-func HasEditPermissionInFolders(query *m.HasEditPermissionInFoldersQuery) error {
-	if query.SignedInUser.HasRole(m.ROLE_EDITOR) {
+func HasEditPermissionInFolders(query *models.HasEditPermissionInFoldersQuery) error {
+	if query.SignedInUser.HasRole(models.ROLE_EDITOR) {
 		query.Result = true
 		return nil
 	}
 
 	builder := &SqlBuilder{}
 	builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgId, dialect.BooleanStr(true))
-	builder.writeDashboardPermissionFilter(query.SignedInUser, m.PERMISSION_EDIT)
+	builder.writeDashboardPermissionFilter(query.SignedInUser, models.PERMISSION_EDIT)
+
+	type folderCount struct {
+		Count int64
+	}
+
+	resp := make([]*folderCount, 0)
+	if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&resp); err != nil {
+		return err
+	}
+
+	query.Result = len(resp) > 0 && resp[0].Count > 0
+
+	return nil
+}
+
+func HasAdminPermissionInFolders(query *models.HasAdminPermissionInFoldersQuery) error {
+	if query.SignedInUser.HasRole(models.ROLE_ADMIN) {
+		query.Result = true
+		return nil
+	}
+
+	builder := &SqlBuilder{}
+	builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgId, dialect.BooleanStr(true))
+	builder.writeDashboardPermissionFilter(query.SignedInUser, models.PERMISSION_ADMIN)
 
 	type folderCount struct {
 		Count int64

+ 79 - 52
pkg/services/sqlstore/dashboard_folder_test.go

@@ -5,7 +5,7 @@ import (
 
 	. "github.com/smartystreets/goconvey/convey"
 
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/search"
 )
 
@@ -24,7 +24,7 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 			Convey("and no acls are set", func() {
 				Convey("should return all dashboards", func() {
 					query := &search.FindPersistedDashboardsQuery{
-						SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+						SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 						OrgId:        1,
 						DashboardIds: []int64{folder.Id, dashInRoot.Id},
 					}
@@ -38,11 +38,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 			Convey("and acl is set for dashboard folder", func() {
 				var otherUser int64 = 999
-				testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
+				testHelperUpdateDashboardAcl(folder.Id, models.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: models.PERMISSION_EDIT})
 
 				Convey("should not return folder", func() {
 					query := &search.FindPersistedDashboardsQuery{
-						SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+						SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 						OrgId:        1, DashboardIds: []int64{folder.Id, dashInRoot.Id},
 					}
 					err := SearchDashboards(query)
@@ -53,11 +53,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("when the user is given permission", func() {
-					testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT})
+					testHelperUpdateDashboardAcl(folder.Id, models.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: currentUser.Id, Permission: models.PERMISSION_EDIT})
 
 					Convey("should be able to access folder", func() {
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+							SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 							OrgId:        1,
 							DashboardIds: []int64{folder.Id, dashInRoot.Id},
 						}
@@ -72,10 +72,10 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				Convey("when the user is an admin", func() {
 					Convey("should be able to access folder", func() {
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{
+							SignedInUser: &models.SignedInUser{
 								UserId:  currentUser.Id,
 								OrgId:   1,
-								OrgRole: m.ROLE_ADMIN,
+								OrgRole: models.ROLE_ADMIN,
 							},
 							OrgId:        1,
 							DashboardIds: []int64{folder.Id, dashInRoot.Id},
@@ -92,10 +92,10 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 			Convey("and acl is set for dashboard child and folder has all permissions removed", func() {
 				var otherUser int64 = 999
 				testHelperUpdateDashboardAcl(folder.Id)
-				testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
+				testHelperUpdateDashboardAcl(childDash.Id, models.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: models.PERMISSION_EDIT})
 
 				Convey("should not return folder or child", func() {
-					query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
+					query := &search.FindPersistedDashboardsQuery{SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
 					err := SearchDashboards(query)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 1)
@@ -103,10 +103,10 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("when the user is given permission to child", func() {
-					testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: childDash.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT})
+					testHelperUpdateDashboardAcl(childDash.Id, models.DashboardAcl{DashboardId: childDash.Id, OrgId: 1, UserId: currentUser.Id, Permission: models.PERMISSION_EDIT})
 
 					Convey("should be able to search for child dashboard but not folder", func() {
-						query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
+						query := &search.FindPersistedDashboardsQuery{SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
 						err := SearchDashboards(query)
 						So(err, ShouldBeNil)
 						So(len(query.Result), ShouldEqual, 2)
@@ -118,10 +118,10 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				Convey("when the user is an admin", func() {
 					Convey("should be able to search for child dash and folder", func() {
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{
+							SignedInUser: &models.SignedInUser{
 								UserId:  currentUser.Id,
 								OrgId:   1,
-								OrgRole: m.ROLE_ADMIN,
+								OrgRole: models.ROLE_ADMIN,
 							},
 							OrgId:        1,
 							DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id},
@@ -149,7 +149,7 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 			Convey("and one folder is expanded, the other collapsed", func() {
 				Convey("should return dashboards in root and expanded folder", func() {
-					query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1}
+					query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, OrgId: 1}
 					err := SearchDashboards(query)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 4)
@@ -162,14 +162,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 			Convey("and acl is set for one dashboard folder", func() {
 				var otherUser int64 = 999
-				testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
+				testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: otherUser, Permission: models.PERMISSION_EDIT})
 
 				Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() {
 					moveDashboard(1, childDash2.Data, folder1.Id)
 
 					Convey("should not return folder with acl or its children", func() {
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+							SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 							OrgId:        1,
 							DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 						}
@@ -184,7 +184,7 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 					Convey("should return folder without acl and its children", func() {
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+							SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 							OrgId:        1,
 							DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 						}
@@ -199,12 +199,12 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("and a dashboard with an acl is moved to the folder without an acl", func() {
-					testHelperUpdateDashboardAcl(childDash1.Id, m.DashboardAcl{DashboardId: childDash1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT})
+					testHelperUpdateDashboardAcl(childDash1.Id, models.DashboardAcl{DashboardId: childDash1.Id, OrgId: 1, UserId: otherUser, Permission: models.PERMISSION_EDIT})
 					moveDashboard(1, childDash1.Data, folder2.Id)
 
 					Convey("should return folder without acl but not the dashboard with acl", func() {
 						query := &search.FindPersistedDashboardsQuery{
-							SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+							SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 							OrgId:        1,
 							DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
 						}
@@ -233,8 +233,8 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				Convey("Should have write access to all dashboard folders in their org", func() {
 					query := search.FindPersistedDashboardsQuery{
 						OrgId:        1,
-						SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN, OrgId: 1},
-						Permission:   m.PERMISSION_VIEW,
+						SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgRole: models.ROLE_ADMIN, OrgId: 1},
+						Permission:   models.PERMISSION_VIEW,
 						Type:         "dash-folder",
 					}
 
@@ -247,11 +247,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("should have write access to all folders and dashboards", func() {
-					query := m.GetDashboardPermissionsForUserQuery{
+					query := models.GetDashboardPermissionsForUserQuery{
 						DashboardIds: []int64{folder1.Id, folder2.Id},
 						OrgId:        1,
 						UserId:       adminUser.Id,
-						OrgRole:      m.ROLE_ADMIN,
+						OrgRole:      models.ROLE_ADMIN,
 					}
 
 					err := GetDashboardPermissionsForUser(&query)
@@ -259,26 +259,35 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 					So(len(query.Result), ShouldEqual, 2)
 					So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
-					So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
+					So(query.Result[0].Permission, ShouldEqual, models.PERMISSION_ADMIN)
 					So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
-					So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_ADMIN)
+					So(query.Result[1].Permission, ShouldEqual, models.PERMISSION_ADMIN)
 				})
 
 				Convey("should have edit permission in folders", func() {
-					query := &m.HasEditPermissionInFoldersQuery{
-						SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: m.ROLE_ADMIN},
+					query := &models.HasEditPermissionInFoldersQuery{
+						SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_ADMIN},
 					}
 					err := HasEditPermissionInFolders(query)
 					So(err, ShouldBeNil)
 					So(query.Result, ShouldBeTrue)
 				})
+
+				Convey("should have admin permission in folders", func() {
+					query := &models.HasAdminPermissionInFoldersQuery{
+						SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_ADMIN},
+					}
+					err := HasAdminPermissionInFolders(query)
+					So(err, ShouldBeNil)
+					So(query.Result, ShouldBeTrue)
+				})
 			})
 
 			Convey("Editor users", func() {
 				query := search.FindPersistedDashboardsQuery{
 					OrgId:        1,
-					SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR, OrgId: 1},
-					Permission:   m.PERMISSION_EDIT,
+					SignedInUser: &models.SignedInUser{UserId: editorUser.Id, OrgRole: models.ROLE_EDITOR, OrgId: 1},
+					Permission:   models.PERMISSION_EDIT,
 				}
 
 				Convey("Should have write access to all dashboard folders with default ACL", func() {
@@ -291,11 +300,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("should have edit access to folders with default ACL", func() {
-					query := m.GetDashboardPermissionsForUserQuery{
+					query := models.GetDashboardPermissionsForUserQuery{
 						DashboardIds: []int64{folder1.Id, folder2.Id},
 						OrgId:        1,
 						UserId:       editorUser.Id,
-						OrgRole:      m.ROLE_EDITOR,
+						OrgRole:      models.ROLE_EDITOR,
 					}
 
 					err := GetDashboardPermissionsForUser(&query)
@@ -303,13 +312,13 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 					So(len(query.Result), ShouldEqual, 2)
 					So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
-					So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
+					So(query.Result[0].Permission, ShouldEqual, models.PERMISSION_EDIT)
 					So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
-					So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_EDIT)
+					So(query.Result[1].Permission, ShouldEqual, models.PERMISSION_EDIT)
 				})
 
 				Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() {
-					testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: editorUser.Id, Permission: m.PERMISSION_VIEW})
+					testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: editorUser.Id, Permission: models.PERMISSION_VIEW})
 
 					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
@@ -319,20 +328,29 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("should have edit permission in folders", func() {
-					query := &m.HasEditPermissionInFoldersQuery{
-						SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgId: 1, OrgRole: m.ROLE_EDITOR},
+					query := &models.HasEditPermissionInFoldersQuery{
+						SignedInUser: &models.SignedInUser{UserId: editorUser.Id, OrgId: 1, OrgRole: models.ROLE_EDITOR},
 					}
 					err := HasEditPermissionInFolders(query)
 					So(err, ShouldBeNil)
 					So(query.Result, ShouldBeTrue)
 				})
+
+				Convey("should not have admin permission in folders", func() {
+					query := &models.HasAdminPermissionInFoldersQuery{
+						SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_EDITOR},
+					}
+					err := HasAdminPermissionInFolders(query)
+					So(err, ShouldBeNil)
+					So(query.Result, ShouldBeFalse)
+				})
 			})
 
 			Convey("Viewer users", func() {
 				query := search.FindPersistedDashboardsQuery{
 					OrgId:        1,
-					SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER, OrgId: 1},
-					Permission:   m.PERMISSION_EDIT,
+					SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgRole: models.ROLE_VIEWER, OrgId: 1},
+					Permission:   models.PERMISSION_EDIT,
 				}
 
 				Convey("Should have no write access to any dashboard folders with default ACL", func() {
@@ -343,11 +361,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("should have view access to folders with default ACL", func() {
-					query := m.GetDashboardPermissionsForUserQuery{
+					query := models.GetDashboardPermissionsForUserQuery{
 						DashboardIds: []int64{folder1.Id, folder2.Id},
 						OrgId:        1,
 						UserId:       viewerUser.Id,
-						OrgRole:      m.ROLE_VIEWER,
+						OrgRole:      models.ROLE_VIEWER,
 					}
 
 					err := GetDashboardPermissionsForUser(&query)
@@ -355,13 +373,13 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 
 					So(len(query.Result), ShouldEqual, 2)
 					So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
-					So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_VIEW)
+					So(query.Result[0].Permission, ShouldEqual, models.PERMISSION_VIEW)
 					So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
-					So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_VIEW)
+					So(query.Result[1].Permission, ShouldEqual, models.PERMISSION_VIEW)
 				})
 
 				Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() {
-					testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT})
+					testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: models.PERMISSION_EDIT})
 
 					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
@@ -371,20 +389,29 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("should not have edit permission in folders", func() {
-					query := &m.HasEditPermissionInFoldersQuery{
-						SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+					query := &models.HasEditPermissionInFoldersQuery{
+						SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 					}
 					err := HasEditPermissionInFolders(query)
 					So(err, ShouldBeNil)
 					So(query.Result, ShouldBeFalse)
 				})
 
+				Convey("should not have admin permission in folders", func() {
+					query := &models.HasAdminPermissionInFoldersQuery{
+						SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
+					}
+					err := HasAdminPermissionInFolders(query)
+					So(err, ShouldBeNil)
+					So(query.Result, ShouldBeFalse)
+				})
+
 				Convey("and admin permission is given for user with org role viewer in one dashboard folder", func() {
-					testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_ADMIN})
+					testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: models.PERMISSION_ADMIN})
 
 					Convey("should have edit permission in folders", func() {
-						query := &m.HasEditPermissionInFoldersQuery{
-							SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+						query := &models.HasEditPermissionInFoldersQuery{
+							SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 						}
 						err := HasEditPermissionInFolders(query)
 						So(err, ShouldBeNil)
@@ -393,11 +420,11 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 				})
 
 				Convey("and edit permission is given for user with org role viewer in one dashboard folder", func() {
-					testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT})
+					testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: models.PERMISSION_EDIT})
 
 					Convey("should have edit permission in folders", func() {
-						query := &m.HasEditPermissionInFoldersQuery{
-							SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
+						query := &models.HasEditPermissionInFoldersQuery{
+							SignedInUser: &models.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER},
 						}
 						err := HasEditPermissionInFolders(query)
 						So(err, ShouldBeNil)

+ 28 - 28
pkg/services/sqlstore/datasource_test.go

@@ -5,7 +5,7 @@ import (
 
 	. "github.com/smartystreets/goconvey/convey"
 
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 )
 
 type Test struct {
@@ -17,11 +17,11 @@ func TestDataAccess(t *testing.T) {
 	Convey("Testing DB", t, func() {
 		InitTestDB(t)
 		Convey("Can add datasource", func() {
-			err := AddDataSource(&m.AddDataSourceCommand{
+			err := AddDataSource(&models.AddDataSourceCommand{
 				OrgId:    10,
 				Name:     "laban",
-				Type:     m.DS_INFLUXDB,
-				Access:   m.DS_ACCESS_DIRECT,
+				Type:     models.DS_INFLUXDB,
+				Access:   models.DS_ACCESS_DIRECT,
 				Url:      "http://test",
 				Database: "site",
 				ReadOnly: true,
@@ -29,7 +29,7 @@ func TestDataAccess(t *testing.T) {
 
 			So(err, ShouldBeNil)
 
-			query := m.GetDataSourcesQuery{OrgId: 10}
+			query := models.GetDataSourcesQuery{OrgId: 10}
 			err = GetDataSources(&query)
 			So(err, ShouldBeNil)
 
@@ -43,28 +43,28 @@ func TestDataAccess(t *testing.T) {
 		})
 
 		Convey("Given a datasource", func() {
-			err := AddDataSource(&m.AddDataSourceCommand{
+			err := AddDataSource(&models.AddDataSourceCommand{
 				OrgId:  10,
 				Name:   "nisse",
-				Type:   m.DS_GRAPHITE,
-				Access: m.DS_ACCESS_DIRECT,
+				Type:   models.DS_GRAPHITE,
+				Access: models.DS_ACCESS_DIRECT,
 				Url:    "http://test",
 			})
 			So(err, ShouldBeNil)
 
-			query := m.GetDataSourcesQuery{OrgId: 10}
+			query := models.GetDataSourcesQuery{OrgId: 10}
 			err = GetDataSources(&query)
 			So(err, ShouldBeNil)
 
 			ds := query.Result[0]
 
 			Convey(" updated ", func() {
-				cmd := &m.UpdateDataSourceCommand{
+				cmd := &models.UpdateDataSourceCommand{
 					Id:      ds.Id,
 					OrgId:   10,
 					Name:    "nisse",
-					Type:    m.DS_GRAPHITE,
-					Access:  m.DS_ACCESS_PROXY,
+					Type:    models.DS_GRAPHITE,
+					Access:  models.DS_ACCESS_PROXY,
 					Url:     "http://test",
 					Version: ds.Version,
 				}
@@ -75,27 +75,27 @@ func TestDataAccess(t *testing.T) {
 				})
 
 				Convey("when someone else updated between read and update", func() {
-					query := m.GetDataSourcesQuery{OrgId: 10}
+					query := models.GetDataSourcesQuery{OrgId: 10}
 					err = GetDataSources(&query)
 					So(err, ShouldBeNil)
 
 					ds := query.Result[0]
-					intendedUpdate := &m.UpdateDataSourceCommand{
+					intendedUpdate := &models.UpdateDataSourceCommand{
 						Id:      ds.Id,
 						OrgId:   10,
 						Name:    "nisse",
-						Type:    m.DS_GRAPHITE,
-						Access:  m.DS_ACCESS_PROXY,
+						Type:    models.DS_GRAPHITE,
+						Access:  models.DS_ACCESS_PROXY,
 						Url:     "http://test",
 						Version: ds.Version,
 					}
 
-					updateFromOtherUser := &m.UpdateDataSourceCommand{
+					updateFromOtherUser := &models.UpdateDataSourceCommand{
 						Id:      ds.Id,
 						OrgId:   10,
 						Name:    "nisse",
-						Type:    m.DS_GRAPHITE,
-						Access:  m.DS_ACCESS_PROXY,
+						Type:    models.DS_GRAPHITE,
+						Access:  models.DS_ACCESS_PROXY,
 						Url:     "http://test",
 						Version: ds.Version,
 					}
@@ -108,12 +108,12 @@ func TestDataAccess(t *testing.T) {
 				})
 
 				Convey("updating datasource without version", func() {
-					cmd := &m.UpdateDataSourceCommand{
+					cmd := &models.UpdateDataSourceCommand{
 						Id:     ds.Id,
 						OrgId:  10,
 						Name:   "nisse",
-						Type:   m.DS_GRAPHITE,
-						Access: m.DS_ACCESS_PROXY,
+						Type:   models.DS_GRAPHITE,
+						Access: models.DS_ACCESS_PROXY,
 						Url:    "http://test",
 					}
 
@@ -124,12 +124,12 @@ func TestDataAccess(t *testing.T) {
 				})
 
 				Convey("updating datasource without higher version", func() {
-					cmd := &m.UpdateDataSourceCommand{
+					cmd := &models.UpdateDataSourceCommand{
 						Id:      ds.Id,
 						OrgId:   10,
 						Name:    "nisse",
-						Type:    m.DS_GRAPHITE,
-						Access:  m.DS_ACCESS_PROXY,
+						Type:    models.DS_GRAPHITE,
+						Access:  models.DS_ACCESS_PROXY,
 						Url:     "http://test",
 						Version: 90000,
 					}
@@ -142,7 +142,7 @@ func TestDataAccess(t *testing.T) {
 			})
 
 			Convey("Can delete datasource by id", func() {
-				err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
+				err := DeleteDataSourceById(&models.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
 				So(err, ShouldBeNil)
 
 				GetDataSources(&query)
@@ -150,7 +150,7 @@ func TestDataAccess(t *testing.T) {
 			})
 
 			Convey("Can delete datasource by name", func() {
-				err := DeleteDataSourceByName(&m.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId})
+				err := DeleteDataSourceByName(&models.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId})
 				So(err, ShouldBeNil)
 
 				GetDataSources(&query)
@@ -158,7 +158,7 @@ func TestDataAccess(t *testing.T) {
 			})
 
 			Convey("Can not delete datasource with wrong orgId", func() {
-				err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123})
+				err := DeleteDataSourceById(&models.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123})
 				So(err, ShouldBeNil)
 
 				GetDataSources(&query)

+ 53 - 34
pkg/services/sqlstore/team.go

@@ -6,7 +6,7 @@ import (
 	"time"
 
 	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 )
 
 func init() {
@@ -21,6 +21,7 @@ func init() {
 	bus.AddHandler("sql", UpdateTeamMember)
 	bus.AddHandler("sql", RemoveTeamMember)
 	bus.AddHandler("sql", GetTeamMembers)
+	bus.AddHandler("sql", IsAdminOfTeams)
 }
 
 func getTeamSearchSqlBase() string {
@@ -45,16 +46,16 @@ func getTeamSelectSqlBase() string {
 		FROM team as team `
 }
 
-func CreateTeam(cmd *m.CreateTeamCommand) error {
+func CreateTeam(cmd *models.CreateTeamCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 
 		if isNameTaken, err := isTeamNameTaken(cmd.OrgId, cmd.Name, 0, sess); err != nil {
 			return err
 		} else if isNameTaken {
-			return m.ErrTeamNameTaken
+			return models.ErrTeamNameTaken
 		}
 
-		team := m.Team{
+		team := models.Team{
 			Name:    cmd.Name,
 			Email:   cmd.Email,
 			OrgId:   cmd.OrgId,
@@ -70,16 +71,16 @@ func CreateTeam(cmd *m.CreateTeamCommand) error {
 	})
 }
 
-func UpdateTeam(cmd *m.UpdateTeamCommand) error {
+func UpdateTeam(cmd *models.UpdateTeamCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 
 		if isNameTaken, err := isTeamNameTaken(cmd.OrgId, cmd.Name, cmd.Id, sess); err != nil {
 			return err
 		} else if isNameTaken {
-			return m.ErrTeamNameTaken
+			return models.ErrTeamNameTaken
 		}
 
-		team := m.Team{
+		team := models.Team{
 			Name:    cmd.Name,
 			Email:   cmd.Email,
 			Updated: time.Now(),
@@ -94,7 +95,7 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
 		}
 
 		if affectedRows == 0 {
-			return m.ErrTeamNotFound
+			return models.ErrTeamNotFound
 		}
 
 		return nil
@@ -102,7 +103,7 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
 }
 
 // DeleteTeam will delete a team, its member and any permissions connected to the team
-func DeleteTeam(cmd *m.DeleteTeamCommand) error {
+func DeleteTeam(cmd *models.DeleteTeamCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		if _, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil {
 			return err
@@ -128,14 +129,14 @@ func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) {
 	if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil {
 		return false, err
 	} else if len(res) != 1 {
-		return false, m.ErrTeamNotFound
+		return false, models.ErrTeamNotFound
 	}
 
 	return true, nil
 }
 
 func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) {
-	var team m.Team
+	var team models.Team
 	exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team)
 
 	if err != nil {
@@ -149,9 +150,9 @@ func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession
 	return false, nil
 }
 
-func SearchTeams(query *m.SearchTeamsQuery) error {
-	query.Result = m.SearchTeamQueryResult{
-		Teams: make([]*m.TeamDTO, 0),
+func SearchTeams(query *models.SearchTeamsQuery) error {
+	query.Result = models.SearchTeamQueryResult{
+		Teams: make([]*models.TeamDTO, 0),
 	}
 	queryWithWildcards := "%" + query.Query + "%"
 
@@ -189,7 +190,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
 		return err
 	}
 
-	team := m.Team{}
+	team := models.Team{}
 	countSess := x.Table("team")
 	if query.Query != "" {
 		countSess.Where(`name `+dialect.LikeStr()+` ?`, queryWithWildcards)
@@ -205,13 +206,13 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
 	return err
 }
 
-func GetTeamById(query *m.GetTeamByIdQuery) error {
+func GetTeamById(query *models.GetTeamByIdQuery) error {
 	var sql bytes.Buffer
 
 	sql.WriteString(getTeamSelectSqlBase())
 	sql.WriteString(` WHERE team.org_id = ? and team.id = ?`)
 
-	var team m.TeamDTO
+	var team models.TeamDTO
 	exists, err := x.SQL(sql.String(), query.OrgId, query.Id).Get(&team)
 
 	if err != nil {
@@ -219,7 +220,7 @@ func GetTeamById(query *m.GetTeamByIdQuery) error {
 	}
 
 	if !exists {
-		return m.ErrTeamNotFound
+		return models.ErrTeamNotFound
 	}
 
 	query.Result = &team
@@ -227,8 +228,8 @@ func GetTeamById(query *m.GetTeamByIdQuery) error {
 }
 
 // GetTeamsByUser is used by the Guardian when checking a users' permissions
-func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
-	query.Result = make([]*m.TeamDTO, 0)
+func GetTeamsByUser(query *models.GetTeamsByUserQuery) error {
+	query.Result = make([]*models.TeamDTO, 0)
 
 	var sql bytes.Buffer
 
@@ -241,19 +242,19 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 }
 
 // AddTeamMember adds a user to a team
-func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
+func AddTeamMember(cmd *models.AddTeamMemberCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil {
 			return err
 		} else if len(res) == 1 {
-			return m.ErrTeamMemberAlreadyAdded
+			return models.ErrTeamMemberAlreadyAdded
 		}
 
 		if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
 			return err
 		}
 
-		entity := m.TeamMember{
+		entity := models.TeamMember{
 			OrgId:      cmd.OrgId,
 			TeamId:     cmd.TeamId,
 			UserId:     cmd.UserId,
@@ -268,23 +269,23 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 	})
 }
 
-func getTeamMember(sess *DBSession, orgId int64, teamId int64, userId int64) (m.TeamMember, error) {
+func getTeamMember(sess *DBSession, orgId int64, teamId int64, userId int64) (models.TeamMember, error) {
 	rawSql := `SELECT * FROM team_member WHERE org_id=? and team_id=? and user_id=?`
-	var member m.TeamMember
+	var member models.TeamMember
 	exists, err := sess.SQL(rawSql, orgId, teamId, userId).Get(&member)
 
 	if err != nil {
 		return member, err
 	}
 	if !exists {
-		return member, m.ErrTeamMemberNotFound
+		return member, models.ErrTeamMemberNotFound
 	}
 
 	return member, nil
 }
 
 // UpdateTeamMember updates a team member
-func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error {
+func UpdateTeamMember(cmd *models.UpdateTeamMemberCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		member, err := getTeamMember(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
 		if err != nil {
@@ -298,7 +299,7 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error {
 			}
 		}
 
-		if cmd.Permission != m.PERMISSION_ADMIN { // make sure we don't get invalid permission levels in store
+		if cmd.Permission != models.PERMISSION_ADMIN { // make sure we don't get invalid permission levels in store
 			cmd.Permission = 0
 		}
 
@@ -310,7 +311,7 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error {
 }
 
 // RemoveTeamMember removes a member from a team
-func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
+func RemoveTeamMember(cmd *models.RemoveTeamMemberCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 		if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
 			return err
@@ -330,7 +331,7 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
 		}
 		rows, err := res.RowsAffected()
 		if rows == 0 {
-			return m.ErrTeamMemberNotFound
+			return models.ErrTeamMemberNotFound
 		}
 
 		return err
@@ -340,7 +341,7 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
 func isLastAdmin(sess *DBSession, orgId int64, teamId int64, userId int64) (bool, error) {
 	rawSql := "SELECT user_id FROM team_member WHERE org_id=? and team_id=? and permission=?"
 	userIds := []*int64{}
-	err := sess.SQL(rawSql, orgId, teamId, m.PERMISSION_ADMIN).Find(&userIds)
+	err := sess.SQL(rawSql, orgId, teamId, models.PERMISSION_ADMIN).Find(&userIds)
 	if err != nil {
 		return false, err
 	}
@@ -354,15 +355,15 @@ func isLastAdmin(sess *DBSession, orgId int64, teamId int64, userId int64) (bool
 	}
 
 	if isAdmin && len(userIds) == 1 {
-		return true, m.ErrLastTeamAdmin
+		return true, models.ErrLastTeamAdmin
 	}
 
 	return false, err
 }
 
 // GetTeamMembers return a list of members for the specified team
-func GetTeamMembers(query *m.GetTeamMembersQuery) error {
-	query.Result = make([]*m.TeamMemberDTO, 0)
+func GetTeamMembers(query *models.GetTeamMembersQuery) error {
+	query.Result = make([]*models.TeamMemberDTO, 0)
 	sess := x.Table("team_member")
 	sess.Join("INNER", x.Dialect().Quote("user"), fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
 
@@ -392,3 +393,21 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
 	err := sess.Find(&query.Result)
 	return err
 }
+
+func IsAdminOfTeams(query *models.IsAdminOfTeamsQuery) error {
+	builder := &SqlBuilder{}
+	builder.Write("SELECT COUNT(team.id) AS count FROM team INNER JOIN team_member ON team_member.team_id = team.id WHERE team.org_id = ? AND team_member.user_id = ? AND team_member.permission = ?", query.SignedInUser.OrgId, query.SignedInUser.UserId, models.PERMISSION_ADMIN)
+
+	type teamCount struct {
+		Count int64
+	}
+
+	resp := make([]*teamCount, 0)
+	if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&resp); err != nil {
+		return err
+	}
+
+	query.Result = len(resp) > 0 && resp[0].Count > 0
+
+	return nil
+}

+ 68 - 50
pkg/services/sqlstore/team_test.go

@@ -7,7 +7,7 @@ import (
 
 	. "github.com/smartystreets/goconvey/convey"
 
-	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/models"
 )
 
 func TestTeamCommandsAndQueries(t *testing.T) {
@@ -18,7 +18,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 		Convey("Given saved users and two teams", func() {
 			var userIds []int64
 			for i := 0; i < 5; i++ {
-				userCmd := &m.CreateUserCommand{
+				userCmd := &models.CreateUserCommand{
 					Email: fmt.Sprint("user", i, "@test.com"),
 					Name:  fmt.Sprint("user", i),
 					Login: fmt.Sprint("loginuser", i),
@@ -29,8 +29,8 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 			}
 
 			var testOrgId int64 = 1
-			group1 := m.CreateTeamCommand{OrgId: testOrgId, Name: "group1 name", Email: "test1@test.com"}
-			group2 := m.CreateTeamCommand{OrgId: testOrgId, Name: "group2 name", Email: "test2@test.com"}
+			group1 := models.CreateTeamCommand{OrgId: testOrgId, Name: "group1 name", Email: "test1@test.com"}
+			group2 := models.CreateTeamCommand{OrgId: testOrgId, Name: "group2 name", Email: "test2@test.com"}
 
 			err := CreateTeam(&group1)
 			So(err, ShouldBeNil)
@@ -38,7 +38,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			Convey("Should be able to create teams and add users", func() {
-				query := &m.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10}
+				query := &models.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10}
 				err = SearchTeams(query)
 				So(err, ShouldBeNil)
 				So(query.Page, ShouldEqual, 1)
@@ -48,12 +48,12 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 				So(team1.Email, ShouldEqual, "test1@test.com")
 				So(team1.OrgId, ShouldEqual, testOrgId)
 
-				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]})
+				err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]})
 				So(err, ShouldBeNil)
-				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[1], External: true})
+				err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[1], External: true})
 				So(err, ShouldBeNil)
 
-				q1 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id}
+				q1 := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id}
 				err = GetTeamMembers(q1)
 				So(err, ShouldBeNil)
 				So(q1.Result, ShouldHaveLength, 2)
@@ -65,7 +65,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 				So(q1.Result[1].OrgId, ShouldEqual, testOrgId)
 				So(q1.Result[1].External, ShouldEqual, true)
 
-				q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true}
+				q2 := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true}
 				err = GetTeamMembers(q2)
 				So(err, ShouldBeNil)
 				So(q2.Result, ShouldHaveLength, 1)
@@ -77,20 +77,20 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 
 			Convey("Should return latest auth module for users when getting team members", func() {
 				userId := userIds[1]
-				err := SetAuthInfo(&m.SetAuthInfoCommand{UserId: userId, AuthModule: "oauth_github", AuthId: "1234567"})
+				err := SetAuthInfo(&models.SetAuthInfoCommand{UserId: userId, AuthModule: "oauth_github", AuthId: "1234567"})
 				So(err, ShouldBeNil)
 
-				teamQuery := &m.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10}
+				teamQuery := &models.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10}
 				err = SearchTeams(teamQuery)
 				So(err, ShouldBeNil)
 				So(teamQuery.Page, ShouldEqual, 1)
 
 				team1 := teamQuery.Result.Teams[0]
 
-				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userId, External: true})
+				err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userId, External: true})
 				So(err, ShouldBeNil)
 
-				memberQuery := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true}
+				memberQuery := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true}
 				err = GetTeamMembers(memberQuery)
 				So(err, ShouldBeNil)
 				So(memberQuery.Result, ShouldHaveLength, 1)
@@ -104,44 +104,44 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 			Convey("Should be able to update users in a team", func() {
 				userId := userIds[0]
 				team := group1.Result
-				addMemberCmd := m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userId}
+				addMemberCmd := models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userId}
 				err = AddTeamMember(&addMemberCmd)
 				So(err, ShouldBeNil)
 
-				qBeforeUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+				qBeforeUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
 				err = GetTeamMembers(qBeforeUpdate)
 				So(err, ShouldBeNil)
 				So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0)
 
-				err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
+				err = UpdateTeamMember(&models.UpdateTeamMemberCommand{
 					UserId:     userId,
 					OrgId:      testOrgId,
 					TeamId:     team.Id,
-					Permission: m.PERMISSION_ADMIN,
+					Permission: models.PERMISSION_ADMIN,
 				})
 
 				So(err, ShouldBeNil)
 
-				qAfterUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+				qAfterUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
 				err = GetTeamMembers(qAfterUpdate)
 				So(err, ShouldBeNil)
-				So(qAfterUpdate.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
+				So(qAfterUpdate.Result[0].Permission, ShouldEqual, models.PERMISSION_ADMIN)
 			})
 
 			Convey("Should default to member permission level when updating a user with invalid permission level", func() {
 				userID := userIds[0]
 				team := group1.Result
-				addMemberCmd := m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userID}
+				addMemberCmd := models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userID}
 				err = AddTeamMember(&addMemberCmd)
 				So(err, ShouldBeNil)
 
-				qBeforeUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+				qBeforeUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
 				err = GetTeamMembers(qBeforeUpdate)
 				So(err, ShouldBeNil)
 				So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0)
 
-				invalidPermissionLevel := m.PERMISSION_EDIT
-				err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
+				invalidPermissionLevel := models.PERMISSION_EDIT
+				err = UpdateTeamMember(&models.UpdateTeamMemberCommand{
 					UserId:     userID,
 					OrgId:      testOrgId,
 					TeamId:     team.Id,
@@ -150,31 +150,31 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 
 				So(err, ShouldBeNil)
 
-				qAfterUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
+				qAfterUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id}
 				err = GetTeamMembers(qAfterUpdate)
 				So(err, ShouldBeNil)
 				So(qAfterUpdate.Result[0].Permission, ShouldEqual, 0)
 			})
 
 			Convey("Shouldn't be able to update a user not in the team.", func() {
-				err = UpdateTeamMember(&m.UpdateTeamMemberCommand{
+				err = UpdateTeamMember(&models.UpdateTeamMemberCommand{
 					UserId:     1,
 					OrgId:      testOrgId,
 					TeamId:     group1.Result.Id,
-					Permission: m.PERMISSION_ADMIN,
+					Permission: models.PERMISSION_ADMIN,
 				})
 
-				So(err, ShouldEqual, m.ErrTeamMemberNotFound)
+				So(err, ShouldEqual, models.ErrTeamMemberNotFound)
 			})
 
 			Convey("Should be able to search for teams", func() {
-				query := &m.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1}
+				query := &models.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1}
 				err = SearchTeams(query)
 				So(err, ShouldBeNil)
 				So(len(query.Result.Teams), ShouldEqual, 2)
 				So(query.Result.TotalCount, ShouldEqual, 2)
 
-				query2 := &m.SearchTeamsQuery{OrgId: testOrgId, Query: ""}
+				query2 := &models.SearchTeamsQuery{OrgId: testOrgId, Query: ""}
 				err = SearchTeams(query2)
 				So(err, ShouldBeNil)
 				So(len(query2.Result.Teams), ShouldEqual, 2)
@@ -182,10 +182,10 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 
 			Convey("Should be able to return all teams a user is member of", func() {
 				groupId := group2.Result.Id
-				err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]})
+				err := AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]})
 				So(err, ShouldBeNil)
 
-				query := &m.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]}
+				query := &models.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]}
 				err = GetTeamsByUser(query)
 				So(err, ShouldBeNil)
 				So(len(query.Result), ShouldEqual, 1)
@@ -194,66 +194,84 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 			})
 
 			Convey("Should be able to remove users from a group", func() {
-				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
+				err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
 				So(err, ShouldBeNil)
 
-				err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
+				err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
 				So(err, ShouldBeNil)
 
-				q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: group1.Result.Id}
+				q2 := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: group1.Result.Id}
 				err = GetTeamMembers(q2)
 				So(err, ShouldBeNil)
 				So(len(q2.Result), ShouldEqual, 0)
 			})
 
 			Convey("When ProtectLastAdmin is set to true", func() {
-				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: m.PERMISSION_ADMIN})
+				err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: models.PERMISSION_ADMIN})
 				So(err, ShouldBeNil)
 
 				Convey("A user should not be able to remove the last admin", func() {
-					err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
-					So(err, ShouldEqual, m.ErrLastTeamAdmin)
+					err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
+					So(err, ShouldEqual, models.ErrLastTeamAdmin)
 				})
 
 				Convey("A user should be able to remove an admin if there are other admins", func() {
-					AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN})
-					err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
+					AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: models.PERMISSION_ADMIN})
+					err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true})
 					So(err, ShouldEqual, nil)
 				})
 
 				Convey("A user should not be able to remove the admin permission for the last admin", func() {
-					err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
-					So(err, ShouldEqual, m.ErrLastTeamAdmin)
+					err = UpdateTeamMember(&models.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
+					So(err, ShouldEqual, models.ErrLastTeamAdmin)
 				})
 
 				Convey("A user should be able to remove the admin permission if there are other admins", func() {
-					AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN})
-					err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
+					AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: models.PERMISSION_ADMIN})
+					err = UpdateTeamMember(&models.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
 					So(err, ShouldEqual, nil)
 				})
 			})
 
 			Convey("Should be able to remove a group with users and permissions", func() {
 				groupId := group2.Result.Id
-				err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]})
+				err := AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]})
 				So(err, ShouldBeNil)
-				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
+				err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
 				So(err, ShouldBeNil)
-				err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId})
+				err = testHelperUpdateDashboardAcl(1, models.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: models.PERMISSION_EDIT, TeamId: groupId})
 				So(err, ShouldBeNil)
-				err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
+				err = DeleteTeam(&models.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
 				So(err, ShouldBeNil)
 
-				query := &m.GetTeamByIdQuery{OrgId: testOrgId, Id: groupId}
+				query := &models.GetTeamByIdQuery{OrgId: testOrgId, Id: groupId}
 				err = GetTeamById(query)
-				So(err, ShouldEqual, m.ErrTeamNotFound)
+				So(err, ShouldEqual, models.ErrTeamNotFound)
 
-				permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: testOrgId}
+				permQuery := &models.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: testOrgId}
 				err = GetDashboardAclInfoList(permQuery)
 				So(err, ShouldBeNil)
 
 				So(len(permQuery.Result), ShouldEqual, 0)
 			})
+
+			Convey("Should be able to return if user is admin of teams or not", func() {
+				groupId := group2.Result.Id
+				err := AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]})
+				So(err, ShouldBeNil)
+				err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1], Permission: models.PERMISSION_ADMIN})
+				So(err, ShouldBeNil)
+
+				query := &models.IsAdminOfTeamsQuery{SignedInUser: &models.SignedInUser{OrgId: testOrgId, UserId: userIds[0]}}
+				err = IsAdminOfTeams(query)
+				So(err, ShouldBeNil)
+				So(query.Result, ShouldBeFalse)
+
+				query = &models.IsAdminOfTeamsQuery{SignedInUser: &models.SignedInUser{OrgId: testOrgId, UserId: userIds[1]}}
+				err = IsAdminOfTeams(query)
+				So(err, ShouldBeNil)
+				So(query.Result, ShouldBeTrue)
+			})
 		})
 	})
 }

+ 2 - 2
public/app/core/components/Select/UserPicker.tsx

@@ -44,12 +44,12 @@ export class UserPicker extends Component<Props, State> {
     }
 
     return backendSrv
-      .get(`/api/org/users?query=${query}&limit=10`)
+      .get(`/api/org/users/lookup?query=${query}&limit=10`)
       .then((result: any) => {
         return result.map((user: any) => ({
           id: user.userId,
           value: user.userId,
-          label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
+          label: user.login,
           imgUrl: user.avatarUrl,
           login: user.login,
         }));

+ 1 - 1
public/app/features/alerting/AlertTabCtrl.ts

@@ -71,7 +71,7 @@ export class AlertTabCtrl {
     this.alertNotifications = [];
     this.alertHistory = [];
 
-    return this.backendSrv.get('/api/alert-notifications').then((res: any) => {
+    return this.backendSrv.get('/api/alert-notifications/lookup').then((res: any) => {
       this.notifications = res;
 
       this.initModel();

+ 1 - 1
public/app/plugins/panel/gettingstarted/GettingStarted.tsx

@@ -79,7 +79,7 @@ export class GettingStarted extends PureComponent<PanelProps, State> {
         href: 'org/users?gettingstarted',
         check: () => {
           return getBackendSrv()
-            .get('/api/org/users')
+            .get('/api/org/users/lookup')
             .then((res: any) => {
               return res.length > 1;
             });