NavTree: Refactor out the navtree building from api/index.go and into it's own service (#55552)

pull/55634/head^2
Torkel Ödegaard 3 years ago committed by GitHub
parent 8863c4c140
commit 09f4068849
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 85
      pkg/api/accesscontrol.go
  2. 42
      pkg/api/api.go
  3. 53
      pkg/api/dtos/index.go
  4. 6
      pkg/api/http_server.go
  5. 739
      pkg/api/index.go
  6. 3
      pkg/api/login_test.go
  7. 21
      pkg/api/navlinks/navlinks.go
  8. 36
      pkg/api/org_test.go
  9. 8
      pkg/api/preferences_test.go
  10. 12
      pkg/api/quota_test.go
  11. 3
      pkg/middleware/middleware_test.go
  12. 2
      pkg/server/wire.go
  13. 59
      pkg/services/accesscontrol/models.go
  14. 3
      pkg/services/licensing/oss.go
  15. 69
      pkg/services/navtree/models.go
  16. 10
      pkg/services/navtree/navtree.go
  17. 116
      pkg/services/navtree/navtreeimpl/admin.go
  18. 113
      pkg/services/navtree/navtreeimpl/applinks.go
  19. 598
      pkg/services/navtree/navtreeimpl/navtree.go
  20. 6
      pkg/services/serviceaccounts/models.go

@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/grafanads" "github.com/grafana/grafana/pkg/tsdb/grafanads"
) )
@ -17,15 +16,6 @@ import (
// API related actions // API related actions
const ( const (
ActionProvisioningReload = "provisioning:reload" ActionProvisioningReload = "provisioning:reload"
ActionOrgsRead = "orgs:read"
ActionOrgsPreferencesRead = "orgs.preferences:read"
ActionOrgsQuotasRead = "orgs.quotas:read"
ActionOrgsWrite = "orgs:write"
ActionOrgsPreferencesWrite = "orgs.preferences:write"
ActionOrgsQuotasWrite = "orgs.quotas:write"
ActionOrgsDelete = "orgs:delete"
ActionOrgsCreate = "orgs:create"
) )
// API related scopes // API related scopes
@ -209,8 +199,8 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Read an organization, such as its ID, name, address, or quotas.", Description: "Read an organization, such as its ID, name, address, or quotas.",
Group: "Organizations", Group: "Organizations",
Permissions: []ac.Permission{ Permissions: []ac.Permission{
{Action: ActionOrgsRead}, {Action: ac.ActionOrgsRead},
{Action: ActionOrgsQuotasRead}, {Action: ac.ActionOrgsQuotasRead},
}, },
}, },
Grants: []string{string(org.RoleViewer), ac.RoleGrafanaAdmin}, Grants: []string{string(org.RoleViewer), ac.RoleGrafanaAdmin},
@ -223,9 +213,9 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Read an organization, its quotas, or its preferences. Update organization properties, or its preferences.", Description: "Read an organization, its quotas, or its preferences. Update organization properties, or its preferences.",
Group: "Organizations", Group: "Organizations",
Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{ Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{
{Action: ActionOrgsPreferencesRead}, {Action: ac.ActionOrgsPreferencesRead},
{Action: ActionOrgsWrite}, {Action: ac.ActionOrgsWrite},
{Action: ActionOrgsPreferencesWrite}, {Action: ac.ActionOrgsPreferencesWrite},
}), }),
}, },
Grants: []string{string(org.RoleAdmin)}, Grants: []string{string(org.RoleAdmin)},
@ -238,10 +228,10 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Create, read, write, or delete an organization. Read or write an organization's quotas. Needs to be assigned globally.", Description: "Create, read, write, or delete an organization. Read or write an organization's quotas. Needs to be assigned globally.",
Group: "Organizations", Group: "Organizations",
Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{ Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{
{Action: ActionOrgsCreate}, {Action: ac.ActionOrgsCreate},
{Action: ActionOrgsWrite}, {Action: ac.ActionOrgsWrite},
{Action: ActionOrgsDelete}, {Action: ac.ActionOrgsDelete},
{Action: ActionOrgsQuotasWrite}, {Action: ac.ActionOrgsQuotasWrite},
}), }),
}, },
Grants: []string{string(ac.RoleGrafanaAdmin)}, Grants: []string{string(ac.RoleGrafanaAdmin)},
@ -443,63 +433,6 @@ func (hs *HTTPServer) declareFixedRoles() error {
) )
} }
// Evaluators
// here is the list of complex evaluators we use in this package
// orgPreferencesAccessEvaluator is used to protect the "Configure > Preferences" page access
var orgPreferencesAccessEvaluator = ac.EvalAny(
ac.EvalAll(
ac.EvalPermission(ActionOrgsRead),
ac.EvalPermission(ActionOrgsWrite),
),
ac.EvalAll(
ac.EvalPermission(ActionOrgsPreferencesRead),
ac.EvalPermission(ActionOrgsPreferencesWrite),
),
)
// orgsAccessEvaluator is used to protect the "Server Admin > Orgs" page access
// (you need to have read access to update or delete orgs; read is the minimum)
var orgsAccessEvaluator = ac.EvalPermission(ActionOrgsRead)
// orgsCreateAccessEvaluator is used to protect the "Server Admin > Orgs > New Org" page access
var orgsCreateAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ActionOrgsRead),
ac.EvalPermission(ActionOrgsCreate),
)
// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access
// grants access to a user when they can either create teams or can read and update a team
var teamsAccessEvaluator = ac.EvalAny(
ac.EvalPermission(ac.ActionTeamsCreate),
ac.EvalAll(
ac.EvalPermission(ac.ActionTeamsRead),
ac.EvalAny(
ac.EvalPermission(ac.ActionTeamsWrite),
ac.EvalPermission(ac.ActionTeamsPermissionsWrite),
),
),
)
// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access
var teamsEditAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ac.ActionTeamsRead),
ac.EvalAny(
ac.EvalPermission(ac.ActionTeamsCreate),
ac.EvalPermission(ac.ActionTeamsWrite),
ac.EvalPermission(ac.ActionTeamsPermissionsWrite),
),
)
// apiKeyAccessEvaluator is used to protect the "Configuration > API keys" page access
var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead)
// serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access
var serviceAccountAccessEvaluator = ac.EvalAny(
ac.EvalPermission(serviceaccounts.ActionRead),
ac.EvalPermission(serviceaccounts.ActionCreate),
)
// Metadata helpers // Metadata helpers
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext,

@ -81,8 +81,8 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/profile/password", reqSignedInNoAnonymous, hs.Index) r.Get("/profile/password", reqSignedInNoAnonymous, hs.Index)
r.Get("/.well-known/change-password", redirectToChangePassword) r.Get("/.well-known/change-password", redirectToChangePassword)
r.Get("/profile/switch-org/:id", reqSignedInNoAnonymous, hs.ChangeActiveOrgAndRedirectToHome) r.Get("/profile/switch-org/:id", reqSignedInNoAnonymous, hs.ChangeActiveOrgAndRedirectToHome)
r.Get("/org/", authorize(reqOrgAdmin, orgPreferencesAccessEvaluator), hs.Index) r.Get("/org/", authorize(reqOrgAdmin, ac.OrgPreferencesAccessEvaluator), hs.Index)
r.Get("/org/new", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, orgsCreateAccessEvaluator), hs.Index) r.Get("/org/new", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.OrgsCreateAccessEvaluator), hs.Index)
r.Get("/datasources/", authorize(reqOrgAdmin, datasources.ConfigurationPageAccess), hs.Index) r.Get("/datasources/", authorize(reqOrgAdmin, datasources.ConfigurationPageAccess), hs.Index)
r.Get("/datasources/new", authorize(reqOrgAdmin, datasources.NewPageAccess), hs.Index) r.Get("/datasources/new", authorize(reqOrgAdmin, datasources.NewPageAccess), hs.Index)
r.Get("/datasources/edit/*", authorize(reqOrgAdmin, datasources.EditPageAccess), hs.Index) r.Get("/datasources/edit/*", authorize(reqOrgAdmin, datasources.EditPageAccess), hs.Index)
@ -91,7 +91,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), hs.Index) r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), hs.Index)
r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, ac.TeamsEditAccessEvaluator), hs.Index)
r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
r.Get("/org/serviceaccounts", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index) r.Get("/org/serviceaccounts", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index)
r.Get("/org/serviceaccounts/:serviceAccountId", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index) r.Get("/org/serviceaccounts/:serviceAccountId", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index)
@ -103,8 +103,8 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/admin/users", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), hs.Index) r.Get("/admin/users", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), hs.Index)
r.Get("/admin/users/create", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index) r.Get("/admin/users/create", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index)
r.Get("/admin/users/edit/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead)), hs.Index) r.Get("/admin/users/edit/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead)), hs.Index)
r.Get("/admin/orgs", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, orgsAccessEvaluator), hs.Index) r.Get("/admin/orgs", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
r.Get("/admin/orgs/edit/:id", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, orgsAccessEvaluator), hs.Index) r.Get("/admin/orgs/edit/:id", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
r.Get("/admin/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index) r.Get("/admin/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
r.Get("/admin/storage/*", reqGrafanaAdmin, hs.Index) r.Get("/admin/storage/*", reqGrafanaAdmin, hs.Index)
r.Get("/admin/ldap", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index) r.Get("/admin/ldap", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index)
@ -251,8 +251,8 @@ func (hs *HTTPServer) registerRoutes() {
// org information available to all users. // org information available to all users.
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(hs.GetCurrentOrg)) orgRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.GetCurrentOrg))
orgRoute.Get("/quotas", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsQuotasRead)), routing.Wrap(hs.GetCurrentOrgQuotas)) orgRoute.Get("/quotas", authorize(reqSignedIn, ac.EvalPermission(ac.ActionOrgsQuotasRead)), routing.Wrap(hs.GetCurrentOrgQuotas))
}) })
if hs.Features.IsEnabled(featuremgmt.FlagStorage) { if hs.Features.IsEnabled(featuremgmt.FlagStorage) {
@ -262,8 +262,8 @@ func (hs *HTTPServer) registerRoutes() {
// current org // current org
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId")) userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))
orgRoute.Put("/", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateCurrentOrg)) orgRoute.Put("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgsWrite)), routing.Wrap(hs.UpdateCurrentOrg))
orgRoute.Put("/address", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateCurrentOrgAddress)) orgRoute.Put("/address", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgsWrite)), routing.Wrap(hs.UpdateCurrentOrgAddress))
orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsersForCurrentOrg)) orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsersForCurrentOrg))
orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsersWithPaging)) orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsersWithPaging))
orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), routing.Wrap(hs.AddOrgUserToCurrentOrg)) orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), routing.Wrap(hs.AddOrgUserToCurrentOrg))
@ -276,9 +276,9 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.RevokeInvite)) orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.RevokeInvite))
// prefs // prefs
orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead)), routing.Wrap(hs.GetOrgPreferences)) orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgsPreferencesRead)), routing.Wrap(hs.GetOrgPreferences))
orgRoute.Put("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesWrite)), routing.Wrap(hs.UpdateOrgPreferences)) orgRoute.Put("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgsPreferencesWrite)), routing.Wrap(hs.UpdateOrgPreferences))
orgRoute.Patch("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesWrite)), routing.Wrap(hs.PatchOrgPreferences)) orgRoute.Patch("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgsPreferencesWrite)), routing.Wrap(hs.PatchOrgPreferences))
}) })
// current org without requirement of user to be org admin // current org without requirement of user to be org admin
@ -299,28 +299,28 @@ func (hs *HTTPServer) registerRoutes() {
}) })
// create new org // create new org
apiRoute.Post("/orgs", authorizeInOrg(reqSignedIn, ac.UseGlobalOrg, ac.EvalPermission(ActionOrgsCreate)), quota("org"), routing.Wrap(hs.CreateOrg)) apiRoute.Post("/orgs", authorizeInOrg(reqSignedIn, ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsCreate)), quota("org"), routing.Wrap(hs.CreateOrg))
// search all orgs // search all orgs
apiRoute.Get("/orgs", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(hs.SearchOrgs)) apiRoute.Get("/orgs", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.SearchOrgs))
// orgs (admin routes) // orgs (admin routes)
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) { apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId")) userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))
orgsRoute.Get("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(hs.GetOrgByID)) orgsRoute.Get("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.GetOrgByID))
orgsRoute.Put("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateOrg)) orgsRoute.Put("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsWrite)), routing.Wrap(hs.UpdateOrg))
orgsRoute.Put("/address", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateOrgAddress)) orgsRoute.Put("/address", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsWrite)), routing.Wrap(hs.UpdateOrgAddress))
orgsRoute.Delete("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsDelete)), routing.Wrap(hs.DeleteOrgByID)) orgsRoute.Delete("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsDelete)), routing.Wrap(hs.DeleteOrgByID))
orgsRoute.Get("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsers)) orgsRoute.Get("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsers))
orgsRoute.Post("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), routing.Wrap(hs.AddOrgUser)) orgsRoute.Post("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), routing.Wrap(hs.AddOrgUser))
orgsRoute.Patch("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser)) orgsRoute.Patch("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser))
orgsRoute.Delete("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser)) orgsRoute.Delete("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser))
orgsRoute.Get("/quotas", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsQuotasRead)), routing.Wrap(hs.GetOrgQuotas)) orgsRoute.Get("/quotas", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsQuotasRead)), routing.Wrap(hs.GetOrgQuotas))
orgsRoute.Put("/quotas/:target", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsQuotasWrite)), routing.Wrap(hs.UpdateOrgQuota)) orgsRoute.Put("/quotas/:target", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsQuotasWrite)), routing.Wrap(hs.UpdateOrgQuota))
}) })
// orgs (admin routes) // orgs (admin routes)
apiRoute.Get("/orgs/name/:name/", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(hs.GetOrgByName)) apiRoute.Get("/orgs/name/:name/", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.GetOrgByName))
// auth api keys // auth api keys
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) { apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {

@ -1,6 +1,7 @@
package dtos package dtos
import ( import (
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"html/template" "html/template"
@ -14,7 +15,7 @@ type IndexViewData struct {
GoogleAnalyticsId string GoogleAnalyticsId string
GoogleAnalytics4Id string GoogleAnalytics4Id string
GoogleTagManagerId string GoogleTagManagerId string
NavTree []*NavLink NavTree []*navtree.NavLink
BuildVersion string BuildVersion string
BuildCommit string BuildCommit string
Theme string Theme string
@ -31,53 +32,3 @@ type IndexViewData struct {
// Nonce is a cryptographic identifier for use with Content Security Policy. // Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string Nonce string
} }
const (
// These weights may be used by an extension to reliably place
// itself in relation to a particular item in the menu. The weights
// are negative to ensure that the default items are placed above
// any items with default weight.
WeightSavedItems = (iota - 20) * 100
WeightCreate
WeightDashboard
WeightExplore
WeightAlerting
WeightDataConnections
WeightPlugin
WeightConfig
WeightAdmin
WeightProfile
WeightHelp
)
const (
NavSectionCore string = "core"
NavSectionPlugin string = "plugin"
NavSectionConfig string = "config"
)
type NavLink struct {
Id string `json:"id,omitempty"`
Text string `json:"text"`
Description string `json:"description,omitempty"`
Section string `json:"section,omitempty"`
SubTitle string `json:"subTitle,omitempty"`
Icon string `json:"icon,omitempty"` // Available icons can be browsed in Storybook: https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
SortWeight int64 `json:"sortWeight,omitempty"`
Divider bool `json:"divider,omitempty"`
HideFromMenu bool `json:"hideFromMenu,omitempty"`
HideFromTabs bool `json:"hideFromTabs,omitempty"`
ShowIconInNavbar bool `json:"showIconInNavbar,omitempty"`
RoundIcon bool `json:"roundIcon,omitempty"`
Children []*NavLink `json:"children,omitempty"`
HighlightText string `json:"highlightText,omitempty"`
HighlightID string `json:"highlightId,omitempty"`
EmptyMessageId string `json:"emptyMessageId,omitempty"`
}
// NavIDCfg is the id for org configuration navigation node
const NavIDCfg = "cfg"

@ -61,6 +61,7 @@ import (
"github.com/grafana/grafana/pkg/services/live/pushhttp" "github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login"
loginAttempt "github.com/grafana/grafana/pkg/services/loginattempt" loginAttempt "github.com/grafana/grafana/pkg/services/loginattempt"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@ -111,6 +112,7 @@ type HTTPServer struct {
Features *featuremgmt.FeatureManager Features *featuremgmt.FeatureManager
SettingsProvider setting.Provider SettingsProvider setting.Provider
HooksService *hooks.HooksService HooksService *hooks.HooksService
navTreeService navtree.Service
CacheService *localcache.CacheService CacheService *localcache.CacheService
DataSourceCache datasources.CacheService DataSourceCache datasources.CacheService
AuthTokenService models.UserTokenService AuthTokenService models.UserTokenService
@ -234,7 +236,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore, secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore,
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service,
loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service, loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service,
accesscontrolService accesscontrol.Service, dashboardThumbsService dashboardThumbs.Service, annotationRepo annotations.Repository, tagService tag.Service, accesscontrolService accesscontrol.Service, dashboardThumbsService dashboardThumbs.Service, navTreeService navtree.Service,
annotationRepo annotations.Repository, tagService tag.Service,
) (*HTTPServer, error) { ) (*HTTPServer, error) {
web.Env = cfg.Env web.Env = cfg.Env
m := web.New() m := web.New()
@ -330,6 +333,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
loginAttemptService: loginAttemptService, loginAttemptService: loginAttemptService,
orgService: orgService, orgService: orgService,
teamService: teamService, teamService: teamService,
navTreeService: navTreeService,
accesscontrolService: accesscontrolService, accesscontrolService: accesscontrolService,
annotationsRepo: annotationRepo, annotationsRepo: annotationRepo,
tagService: tagService, tagService: tagService,

@ -3,22 +3,15 @@ package api
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"sort" "sort"
"strings" "strings"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/navlinks"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
pref "github.com/grafana/grafana/pkg/services/preference" pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -28,736 +21,6 @@ const (
darkName = "dark" darkName = "dark"
) )
func (hs *HTTPServer) getProfileNode(c *models.ReqContext) *dtos.NavLink {
// Only set login if it's different from the name
var login string
if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
login = c.SignedInUser.Login
}
gravatarURL := dtos.GetGravatarUrl(c.Email)
children := []*dtos.NavLink{
{
Text: "Preferences", Id: "profile/settings", Url: hs.Cfg.AppSubURL + "/profile", Icon: "sliders-v-alt",
},
}
children = append(children, &dtos.NavLink{
Text: "Notification history", Id: "profile/notifications", Url: hs.Cfg.AppSubURL + "/profile/notifications", Icon: "bell",
})
if setting.AddChangePasswordLink() {
children = append(children, &dtos.NavLink{
Text: "Change password", Id: "profile/password", Url: hs.Cfg.AppSubURL + "/profile/password",
Icon: "lock",
})
}
if !setting.DisableSignoutMenu {
// add sign out first
children = append(children, &dtos.NavLink{
Text: "Sign out",
Id: "sign-out",
Url: hs.Cfg.AppSubURL + "/logout",
Icon: "arrow-from-right",
Target: "_self",
HideFromTabs: true,
})
}
return &dtos.NavLink{
Text: c.SignedInUser.NameOrFallback(),
SubTitle: login,
Id: "profile",
Img: gravatarURL,
Url: hs.Cfg.AppSubURL + "/profile",
Section: dtos.NavSectionConfig,
SortWeight: dtos.WeightProfile,
Children: children,
RoundIcon: true,
}
}
func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) {
hasAccess := ac.HasAccess(hs.AccessControl, c)
enabledPlugins, err := hs.enabledPlugins(c.Req.Context(), c.OrgID)
if err != nil {
return nil, err
}
appLinks := []*dtos.NavLink{}
for _, plugin := range enabledPlugins[plugins.App] {
if !plugin.Pinned {
continue
}
if !hasAccess(ac.ReqSignedIn,
ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) {
continue
}
appLink := &dtos.NavLink{
Text: plugin.Name,
Id: "plugin-page-" + plugin.ID,
Img: plugin.Info.Logos.Small,
Section: dtos.NavSectionPlugin,
SortWeight: dtos.WeightPlugin,
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
appLink.Url = hs.Cfg.AppSubURL + "/a/" + plugin.ID
} else {
appLink.Url = path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL)
}
for _, include := range plugin.Includes {
if !c.HasUserRole(include.Role) {
continue
}
if include.Type == "page" && include.AddToNav {
var link *dtos.NavLink
if len(include.Path) > 0 {
link = &dtos.NavLink{
Url: hs.Cfg.AppSubURL + include.Path,
Text: include.Name,
}
if include.DefaultNav && !hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
appLink.Url = link.Url // Overwrite the hardcoded page logic
}
} else {
link = &dtos.NavLink{
Url: hs.Cfg.AppSubURL + "/plugins/" + plugin.ID + "/page/" + include.Slug,
Text: include.Name,
}
}
link.Icon = include.Icon
appLink.Children = append(appLink.Children, link)
}
if include.Type == "dashboard" && include.AddToNav {
dboardURL := include.DashboardURLPath()
if dboardURL != "" {
link := &dtos.NavLink{
Url: path.Join(hs.Cfg.AppSubURL, dboardURL),
Text: include.Name,
}
appLink.Children = append(appLink.Children, link)
}
}
}
if len(appLink.Children) > 0 {
// If we only have one child and it's the app default nav then remove it from children
if len(appLink.Children) == 1 && appLink.Children[0].Url == appLink.Url {
appLink.Children = []*dtos.NavLink{}
}
appLinks = append(appLinks, appLink)
}
}
if len(appLinks) > 0 {
sort.SliceStable(appLinks, func(i, j int) bool {
return appLinks[i].Text < appLinks[j].Text
})
}
return appLinks, nil
}
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
hasAccess := ac.HasAccess(hs.AccessControl, c)
return hasAccess(ac.ReqOrgAdmin, serviceAccountAccessEvaluator)
}
func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool {
return c.OrgRole == org.RoleAdmin || (hs.Cfg.EditorsCanAdmin && c.OrgRole == org.RoleEditor)
}
//nolint:gocyclo
func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) ([]*dtos.NavLink, error) {
hasAccess := ac.HasAccess(hs.AccessControl, c)
var navTree []*dtos.NavLink
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsRead)) {
starredItemsLinks, err := hs.buildStarredItemsNavLinks(c, prefs)
if err != nil {
return nil, err
}
navTree = append(navTree, &dtos.NavLink{
Text: "Starred",
Id: "starred",
Icon: "star",
SortWeight: dtos.WeightSavedItems,
Section: dtos.NavSectionCore,
Children: starredItemsLinks,
EmptyMessageId: "starred-empty",
})
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
dashboardsUrl := "/dashboards"
dashboardLink := &dtos.NavLink{
Text: "Dashboards",
Id: "dashboards",
SubTitle: "Manage dashboards and folders",
Icon: "apps",
Url: hs.Cfg.AppSubURL + dashboardsUrl,
SortWeight: dtos.WeightDashboard,
Section: dtos.NavSectionCore,
Children: dashboardChildLinks,
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
dashboardLink.Id = "dashboards/browse"
}
navTree = append(navTree, dashboardLink)
}
canExplore := func(context *models.ReqContext) bool {
return c.OrgRole == org.RoleAdmin || c.OrgRole == org.RoleEditor || setting.ViewersCanEdit
}
if setting.ExploreEnabled && hasAccess(canExplore, ac.EvalPermission(ac.ActionDatasourcesExplore)) {
navTree = append(navTree, &dtos.NavLink{
Text: "Explore",
Id: "explore",
SubTitle: "Explore your data",
Icon: "compass",
SortWeight: dtos.WeightExplore,
Section: dtos.NavSectionCore,
Url: hs.Cfg.AppSubURL + "/explore",
})
}
navTree = hs.addProfile(navTree, c)
_, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgID]
uaVisibleForOrg := hs.Cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg
if setting.AlertingEnabled != nil && *setting.AlertingEnabled {
navTree = append(navTree, hs.buildLegacyAlertNavLinks(c)...)
} else if uaVisibleForOrg {
navTree = append(navTree, hs.buildAlertNavLinks(c)...)
}
if hs.Features.IsEnabled(featuremgmt.FlagDataConnectionsConsole) {
navTree = append(navTree, hs.buildDataConnectionsNavLink(c))
}
appLinks, err := hs.getAppLinks(c)
if err != nil {
return nil, err
}
// When topnav is enabled we can test new information architecture where plugins live in Apps category
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
navTree = append(navTree, &dtos.NavLink{
Text: "Apps",
Icon: "apps",
Description: "App plugins",
Id: "apps",
Children: appLinks,
Section: dtos.NavSectionCore,
Url: hs.Cfg.AppSubURL + "/apps",
})
} else {
navTree = append(navTree, appLinks...)
}
configNodes, err := hs.setupConfigNodes(c)
if err != nil {
return navTree, err
}
if hs.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
liveNavLinks := []*dtos.NavLink{}
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
Text: "Status", Id: "live-status", Url: hs.Cfg.AppSubURL + "/live", Icon: "exchange-alt",
})
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
Text: "Pipeline", Id: "live-pipeline", Url: hs.Cfg.AppSubURL + "/live/pipeline", Icon: "arrow-to-right",
})
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
Text: "Cloud", Id: "live-cloud", Url: hs.Cfg.AppSubURL + "/live/cloud", Icon: "cloud-upload",
})
navTree = append(navTree, &dtos.NavLink{
Id: "live",
Text: "Live",
SubTitle: "Event streaming",
Icon: "exchange-alt",
Url: hs.Cfg.AppSubURL + "/live",
Children: liveNavLinks,
Section: dtos.NavSectionConfig,
HideFromTabs: true,
})
}
var configNode *dtos.NavLink
var serverAdminNode *dtos.NavLink
if len(configNodes) > 0 {
configNode = &dtos.NavLink{
Id: dtos.NavIDCfg,
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "cog",
Url: configNodes[0].Url,
Section: dtos.NavSectionConfig,
SortWeight: dtos.WeightConfig,
Children: configNodes,
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
configNode.Url = "/admin"
} else {
configNode.Url = configNodes[0].Url
}
navTree = append(navTree, configNode)
}
adminNavLinks := hs.buildAdminNavLinks(c)
if len(adminNavLinks) > 0 {
serverAdminNode = navlinks.GetServerAdminNode(adminNavLinks)
navTree = append(navTree, serverAdminNode)
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
// Move server admin into Configuration and rename to administration
if configNode != nil && serverAdminNode != nil {
configNode.Text = "Administration"
serverAdminNode.Url = "/admin/server"
serverAdminNode.HideFromTabs = false
configNode.Children = append(configNode.Children, serverAdminNode)
adminNodeIndex := len(navTree) - 1
navTree = navTree[:adminNodeIndex]
}
}
navTree = hs.addHelpLinks(navTree, c)
return navTree, nil
}
func (hs *HTTPServer) setupConfigNodes(c *models.ReqContext) ([]*dtos.NavLink, error) {
var configNodes []*dtos.NavLink
hasAccess := ac.HasAccess(hs.AccessControl, c)
if hasAccess(ac.ReqOrgAdmin, datasources.ConfigurationPageAccess) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Data sources",
Icon: "database",
Description: "Add and configure data sources",
Id: "datasources",
Url: hs.Cfg.AppSubURL + "/datasources",
})
}
if hs.Features.IsEnabled(featuremgmt.FlagCorrelations) && hasAccess(ac.ReqOrgAdmin, correlations.ConfigurationPageAccess) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Correlations",
Icon: "gf-glue",
Description: "Add and configure correlations",
Id: "correlations",
Url: hs.Cfg.AppSubURL + "/datasources/correlations",
})
}
if hasAccess(ac.ReqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Users",
Id: "users",
Description: "Manage org members",
Icon: "user",
Url: hs.Cfg.AppSubURL + "/org/users",
})
}
if hasAccess(hs.ReqCanAdminTeams, teamsAccessEvaluator) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Teams",
Id: "teams",
Description: "Manage org groups",
Icon: "users-alt",
Url: hs.Cfg.AppSubURL + "/org/teams",
})
}
// FIXME: while we don't have a permissions for listing plugins the legacy check has to stay as a default
if plugins.ReqCanAdminPlugins(hs.Cfg)(c) || hasAccess(plugins.ReqCanAdminPlugins(hs.Cfg), plugins.AdminAccessEvaluator) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Plugins",
Id: "plugins",
Description: "View and configure plugins",
Icon: "plug",
Url: hs.Cfg.AppSubURL + "/plugins",
})
}
if hasAccess(ac.ReqOrgAdmin, orgPreferencesAccessEvaluator) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Preferences",
Id: "org-settings",
Description: "Organization preferences",
Icon: "sliders-v-alt",
Url: hs.Cfg.AppSubURL + "/org",
})
}
hideApiKeys, _, _ := hs.kvStore.Get(c.Req.Context(), c.OrgID, "serviceaccounts", "hideApiKeys")
apiKeys, err := hs.apiKeyService.GetAllAPIKeys(c.Req.Context(), c.OrgID)
if err != nil {
return nil, err
}
apiKeysHidden := hideApiKeys == "1" && len(apiKeys) == 0
if hasAccess(ac.ReqOrgAdmin, apiKeyAccessEvaluator) && !apiKeysHidden {
configNodes = append(configNodes, &dtos.NavLink{
Text: "API keys",
Id: "apikeys",
Description: "Create & manage API keys",
Icon: "key-skeleton-alt",
Url: hs.Cfg.AppSubURL + "/org/apikeys",
})
}
if enableServiceAccount(hs, c) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Service accounts",
Id: "serviceaccounts",
Description: "Manage service accounts",
Icon: "gf-service-account",
Url: hs.Cfg.AppSubURL + "/org/serviceaccounts",
})
}
return configNodes, nil
}
func (hs *HTTPServer) addProfile(navTree []*dtos.NavLink, c *models.ReqContext) []*dtos.NavLink {
if setting.ProfileEnabled && c.IsSignedIn {
navTree = append(navTree, hs.getProfileNode(c))
}
return navTree
}
func (hs *HTTPServer) addHelpLinks(navTree []*dtos.NavLink, c *models.ReqContext) []*dtos.NavLink {
if setting.HelpEnabled {
helpVersion := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit)
if hs.Cfg.AnonymousHideVersion && !c.IsSignedIn {
helpVersion = setting.ApplicationName
}
navTree = append(navTree, &dtos.NavLink{
Text: "Help",
SubTitle: helpVersion,
Id: "help",
Url: "#",
Icon: "question-circle",
SortWeight: dtos.WeightHelp,
Section: dtos.NavSectionConfig,
Children: []*dtos.NavLink{},
})
}
return navTree
}
func (hs *HTTPServer) buildStarredItemsNavLinks(c *models.ReqContext, prefs *pref.Preference) ([]*dtos.NavLink, error) {
starredItemsChildNavs := []*dtos.NavLink{}
query := star.GetUserStarsQuery{
UserID: c.SignedInUser.UserID,
}
starredDashboardResult, err := hs.starService.GetByUser(c.Req.Context(), &query)
if err != nil {
return nil, err
}
starredDashboards := []*models.Dashboard{}
starredDashboardsCounter := 0
for dashboardId := range starredDashboardResult.UserStars {
// Set a loose limit to the first 50 starred dashboards found
if starredDashboardsCounter > 50 {
break
}
starredDashboardsCounter++
query := &models.GetDashboardQuery{
Id: dashboardId,
OrgId: c.OrgID,
}
err := hs.DashboardService.GetDashboard(c.Req.Context(), query)
if err == nil {
starredDashboards = append(starredDashboards, query.Result)
}
}
if len(starredDashboards) > 0 {
sort.Slice(starredDashboards, func(i, j int) bool {
return starredDashboards[i].Title < starredDashboards[j].Title
})
for _, starredItem := range starredDashboards {
starredItemsChildNavs = append(starredItemsChildNavs, &dtos.NavLink{
Id: starredItem.Uid,
Text: starredItem.Title,
Url: starredItem.GetUrl(),
})
}
}
return starredItemsChildNavs, nil
}
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
hasAccess := ac.HasAccess(hs.AccessControl, c)
hasEditPermInAnyFolder := func(c *models.ReqContext) bool {
return hasEditPerm
}
dashboardChildNavs := []*dtos.NavLink{}
if !hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Browse", Id: "dashboards/browse", Url: hs.Cfg.AppSubURL + "/dashboards", Icon: "sitemap",
})
}
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Playlists", Id: "dashboards/playlists", Url: hs.Cfg.AppSubURL + "/playlists", Icon: "presentation-play",
})
if c.IsSignedIn {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Snapshots",
Id: "dashboards/snapshots",
Url: hs.Cfg.AppSubURL + "/dashboard/snapshots",
Icon: "camera",
})
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Library panels",
Id: "dashboards/library-panels",
Url: hs.Cfg.AppSubURL + "/library-panels",
Icon: "library-panel",
})
}
if hs.Features.IsEnabled(featuremgmt.FlagScenes) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Scenes",
Id: "scenes",
Url: hs.Cfg.AppSubURL + "/scenes",
Icon: "apps",
})
}
if hasEditPerm {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
})
if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "New dashboard", Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "dashboards/new", ShowIconInNavbar: true,
})
}
if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalPermission(dashboards.ActionFoldersCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "New folder", SubTitle: "Create a new folder to organize your dashboards", Id: "dashboards/folder/new",
Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new", HideFromTabs: true, ShowIconInNavbar: true,
})
}
if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "dashboards/import", Icon: "plus",
Url: hs.Cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, ShowIconInNavbar: true,
})
}
}
return dashboardChildNavs
}
func (hs *HTTPServer) buildLegacyAlertNavLinks(c *models.ReqContext) []*dtos.NavLink {
var alertChildNavs []*dtos.NavLink
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
})
if c.HasRole(org.RoleEditor) {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Notification channels", Id: "channels", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share",
})
}
var alertNav = dtos.NavLink{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting-legacy",
Icon: "bell",
Children: alertChildNavs,
Section: dtos.NavSectionCore,
SortWeight: dtos.WeightAlerting,
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
alertNav.Url = hs.Cfg.AppSubURL + "/alerting"
} else {
alertNav.Url = hs.Cfg.AppSubURL + "/alerting/list"
}
return []*dtos.NavLink{&alertNav}
}
func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext) []*dtos.NavLink {
hasAccess := ac.HasAccess(hs.AccessControl, c)
var alertChildNavs []*dtos.NavLink
if hasAccess(ac.ReqViewer, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
})
}
if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsRead), ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead))) {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Contact points", Id: "receivers", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share", SubTitle: "Manage the settings of your contact points",
})
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Notification policies", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
}
if hasAccess(ac.ReqViewer, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceRead), ac.EvalPermission(ac.ActionAlertingInstancesExternalRead))) {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Alert groups", Id: "groups", Url: hs.Cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
}
if c.OrgRole == org.RoleAdmin {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Admin", Id: "alerting-admin", Url: hs.Cfg.AppSubURL + "/alerting/admin",
Icon: "cog",
})
}
if hasAccess(hs.editorInAnyFolder, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingRuleExternalWrite))) {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
})
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "New alert rule", SubTitle: "Create an alert rule", Id: "alert",
Icon: "plus", Url: hs.Cfg.AppSubURL + "/alerting/new", HideFromTabs: true, ShowIconInNavbar: true,
})
}
if len(alertChildNavs) > 0 {
var alertNav = dtos.NavLink{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting",
Icon: "bell",
Children: alertChildNavs,
Section: dtos.NavSectionCore,
SortWeight: dtos.WeightAlerting,
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
alertNav.Url = hs.Cfg.AppSubURL + "/alerting"
} else {
alertNav.Url = hs.Cfg.AppSubURL + "/alerting/list"
}
return []*dtos.NavLink{&alertNav}
}
return nil
}
func (hs *HTTPServer) buildDataConnectionsNavLink(c *models.ReqContext) *dtos.NavLink {
var children []*dtos.NavLink
var navLink *dtos.NavLink
baseId := "data-connections"
baseUrl := hs.Cfg.AppSubURL + "/" + baseId
children = append(children, &dtos.NavLink{
Id: baseId + "-datasources",
Text: "Data sources",
Icon: "database",
Description: "Add and configure data sources",
Url: baseUrl + "/datasources",
})
children = append(children, &dtos.NavLink{
Id: baseId + "-plugins",
Text: "Plugins",
Icon: "plug",
Description: "Manage plugins",
Url: baseUrl + "/plugins",
})
children = append(children, &dtos.NavLink{
Id: baseId + "-cloud-integrations",
Text: "Cloud integrations",
Icon: "bolt",
Description: "Manage your cloud integrations",
Url: baseUrl + "/cloud-integrations",
})
navLink = &dtos.NavLink{
Text: "Data Connections",
Icon: "link",
Id: baseId,
Url: baseUrl,
Children: children,
Section: dtos.NavSectionCore,
SortWeight: dtos.WeightDataConnections,
}
return navLink
}
func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
hasAccess := ac.HasAccess(hs.AccessControl, c)
hasGlobalAccess := ac.HasGlobalAccess(hs.AccessControl, hs.accesscontrolService, c)
adminNavLinks := []*dtos.NavLink{}
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Users", Id: "global-users", Url: hs.Cfg.AppSubURL + "/admin/users", Icon: "user",
})
}
if hasGlobalAccess(ac.ReqGrafanaAdmin, orgsAccessEvaluator) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Orgs", Id: "global-orgs", Url: hs.Cfg.AppSubURL + "/admin/orgs", Icon: "building",
})
}
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Settings", Id: "server-settings", Url: hs.Cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
})
}
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) && hs.Features.IsEnabled(featuremgmt.FlagStorage) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Storage",
Id: "storage",
Description: "Manage file storage",
Icon: "cube",
Url: hs.Cfg.AppSubURL + "/admin/storage",
})
}
if hs.Cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "LDAP", Id: "ldap", Url: hs.Cfg.AppSubURL + "/admin/ldap", Icon: "book",
})
}
return adminNavLinks
}
func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool {
hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { if err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
@ -806,7 +69,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
settings["appSubUrl"] = "" settings["appSubUrl"] = ""
} }
navTree, err := hs.getNavTree(c, hasEditPerm, prefs) navTree, err := hs.navTreeService.GetNavTree(c, hasEditPerm, prefs)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -13,6 +13,7 @@ import (
"testing" "testing"
loginservice "github.com/grafana/grafana/pkg/services/login" loginservice "github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -43,7 +44,7 @@ func fakeSetIndexViewData(t *testing.T) {
data := &dtos.IndexViewData{ data := &dtos.IndexViewData{
User: &dtos.CurrentUser{}, User: &dtos.CurrentUser{},
Settings: map[string]interface{}{}, Settings: map[string]interface{}{},
NavTree: []*dtos.NavLink{}, NavTree: []*navtree.NavLink{},
} }
return data, nil return data, nil
} }

@ -1,21 +0,0 @@
package navlinks
import "github.com/grafana/grafana/pkg/api/dtos"
func GetServerAdminNode(children []*dtos.NavLink) *dtos.NavLink {
url := ""
if len(children) > 0 {
url = children[0].Url
}
return &dtos.NavLink{
Text: "Server admin",
SubTitle: "Manage all users and orgs",
HideFromTabs: true,
Id: "admin",
Icon: "shield",
Url: url,
SortWeight: dtos.WeightAdmin,
Section: dtos.NavSectionConfig,
Children: children,
}
}

@ -70,12 +70,12 @@ func TestAPIEndpoint_GetCurrentOrg_AccessControl(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
t.Run("AccessControl allows viewing CurrentOrg with correct permissions", func(t *testing.T) { t.Run("AccessControl allows viewing CurrentOrg with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsRead}}, sc.initCtx.OrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsRead}}, sc.initCtx.OrgID)
response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t) response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents viewing CurrentOrg with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents viewing CurrentOrg with correct permissions in another org", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsRead}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsRead}}, 2)
response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t) response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -121,13 +121,13 @@ func TestAPIEndpoint_PutCurrentOrg_AccessControl(t *testing.T) {
input := strings.NewReader(testUpdateOrgNameForm) input := strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl allows updating current org with correct permissions", func(t *testing.T) { t.Run("AccessControl allows updating current org with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, sc.initCtx.OrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, sc.initCtx.OrgID)
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t) response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents updating current org with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents updating current org with correct permissions in another org", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, 2)
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t) response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -171,14 +171,14 @@ func TestAPIEndpoint_PutCurrentOrgAddress_AccessControl(t *testing.T) {
input := strings.NewReader(testUpdateOrgAddressForm) input := strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl allows updating current org address with correct permissions", func(t *testing.T) { t.Run("AccessControl allows updating current org address with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, sc.initCtx.OrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, sc.initCtx.OrgID)
response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t) response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
input = strings.NewReader(testUpdateOrgAddressForm) input = strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl prevents updating current org address with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents updating current org address with correct permissions in another org", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, 2)
response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t) response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -243,7 +243,7 @@ func TestAPIEndpoint_CreateOrgs_AccessControl(t *testing.T) {
input := strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 2)) input := strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 2))
t.Run("AccessControl allows creating Orgs with correct permissions", func(t *testing.T) { t.Run("AccessControl allows creating Orgs with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsCreate}}, accesscontrol.GlobalOrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsCreate}}, accesscontrol.GlobalOrgID)
response := callAPI(sc.server, http.MethodPost, createOrgsURL, input, t) response := callAPI(sc.server, http.MethodPost, createOrgsURL, input, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
@ -289,13 +289,13 @@ func TestAPIEndpoint_DeleteOrgs_AccessControl(t *testing.T) {
}) })
t.Run("AccessControl prevents deleting Orgs with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents deleting Orgs with correct permissions in another org", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsDelete}}, 1) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsDelete}}, 1)
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t) response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
t.Run("AccessControl allows deleting Orgs with correct permissions", func(t *testing.T) { t.Run("AccessControl allows deleting Orgs with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsDelete}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsDelete}}, 2)
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t) response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
@ -324,13 +324,13 @@ func TestAPIEndpoint_SearchOrgs_AccessControl(t *testing.T) {
t.Run("AccessControl allows listing Orgs with correct permissions", func(t *testing.T) { t.Run("AccessControl allows listing Orgs with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsRead}}, accesscontrol.GlobalOrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsRead}}, accesscontrol.GlobalOrgID)
response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t) response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents listing Orgs with correct permissions not granted globally", func(t *testing.T) { t.Run("AccessControl prevents listing Orgs with correct permissions not granted globally", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsRead}}, 1) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsRead}}, 1)
response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t) response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -371,13 +371,13 @@ func TestAPIEndpoint_GetOrg_AccessControl(t *testing.T) {
t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) { t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsRead}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsRead}}, 2)
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t) response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents viewing another org with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents viewing another org with correct permissions in another org", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsRead}}, 1) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsRead}}, 1)
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t) response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -418,7 +418,7 @@ func TestAPIEndpoint_GetOrgByName_AccessControl(t *testing.T) {
t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) { t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsRead}}, accesscontrol.GlobalOrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsRead}}, accesscontrol.GlobalOrgID)
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t) response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
@ -462,14 +462,14 @@ func TestAPIEndpoint_PutOrg_AccessControl(t *testing.T) {
input := strings.NewReader(testUpdateOrgNameForm) input := strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl allows updating another org with correct permissions", func(t *testing.T) { t.Run("AccessControl allows updating another org with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, 2)
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents updating another org with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents updating another org with correct permissions in another org", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, 1) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, 1)
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -514,7 +514,7 @@ func TestAPIEndpoint_PutOrgAddress_AccessControl(t *testing.T) {
input := strings.NewReader(testUpdateOrgAddressForm) input := strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl allows updating another org address with correct permissions", func(t *testing.T) { t.Run("AccessControl allows updating another org address with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, 2)
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
@ -522,7 +522,7 @@ func TestAPIEndpoint_PutOrgAddress_AccessControl(t *testing.T) {
input = strings.NewReader(testUpdateOrgAddressForm) input = strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl prevents updating another org address with correct permissions in the current org", func(t *testing.T) { t.Run("AccessControl prevents updating another org address with correct permissions in the current org", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsWrite}}, 1) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsWrite}}, 1)
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })

@ -83,12 +83,12 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_AccessControl(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
t.Run("AccessControl allows getting org preferences with correct permissions", func(t *testing.T) { t.Run("AccessControl allows getting org preferences with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsPreferencesRead}}, sc.initCtx.OrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsPreferencesRead}}, sc.initCtx.OrgID)
response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t) response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents getting org preferences with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents getting org preferences with correct permissions in another org", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsPreferencesRead}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsPreferencesRead}}, 2)
response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t) response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -131,14 +131,14 @@ func TestAPIEndpoint_PutCurrentOrgPreferences_AccessControl(t *testing.T) {
input := strings.NewReader(testUpdateOrgPreferencesCmd) input := strings.NewReader(testUpdateOrgPreferencesCmd)
t.Run("AccessControl allows updating org preferences with correct permissions", func(t *testing.T) { t.Run("AccessControl allows updating org preferences with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsPreferencesWrite}}, sc.initCtx.OrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsPreferencesWrite}}, sc.initCtx.OrgID)
response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t) response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
input = strings.NewReader(testUpdateOrgPreferencesCmd) input = strings.NewReader(testUpdateOrgPreferencesCmd)
t.Run("AccessControl prevents updating org preferences with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents updating org preferences with correct permissions in another org", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsPreferencesWrite}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsPreferencesWrite}}, 2)
response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t) response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })

@ -68,12 +68,12 @@ func TestAPIEndpoint_GetCurrentOrgQuotas_AccessControl(t *testing.T) {
setupDBAndSettingsForAccessControlQuotaTests(t, sc) setupDBAndSettingsForAccessControlQuotaTests(t, sc)
t.Run("AccessControl allows viewing CurrentOrgQuotas with correct permissions", func(t *testing.T) { t.Run("AccessControl allows viewing CurrentOrgQuotas with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, sc.initCtx.OrgID) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, sc.initCtx.OrgID)
response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents viewing CurrentOrgQuotas with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents viewing CurrentOrgQuotas with correct permissions in another org", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 2)
response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -110,13 +110,13 @@ func TestAPIEndpoint_GetOrgQuotas_AccessControl(t *testing.T) {
t.Run("AccessControl allows viewing another org quotas with correct permissions", func(t *testing.T) { t.Run("AccessControl allows viewing another org quotas with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 2)
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
t.Run("AccessControl prevents viewing another org quotas with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents viewing another org quotas with correct permissions in another org", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, 1) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 1)
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })
@ -157,7 +157,7 @@ func TestAPIEndpoint_PutOrgQuotas_AccessControl(t *testing.T) {
input := strings.NewReader(testUpdateOrgQuotaCmd) input := strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("AccessControl allows updating another org quotas with correct permissions", func(t *testing.T) { t.Run("AccessControl allows updating another org quotas with correct permissions", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsQuotasWrite}}, 2) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}, 2)
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, http.StatusOK, response.Code)
}) })
@ -165,7 +165,7 @@ func TestAPIEndpoint_PutOrgQuotas_AccessControl(t *testing.T) {
input = strings.NewReader(testUpdateOrgQuotaCmd) input = strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("AccessControl prevents updating another org quotas with correct permissions in another org", func(t *testing.T) { t.Run("AccessControl prevents updating another org quotas with correct permissions in another org", func(t *testing.T) {
setInitCtxSignedInViewer(sc.initCtx) setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: ActionOrgsQuotasWrite}}, 1) setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}, 1)
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusForbidden, response.Code) assert.Equal(t, http.StatusForbidden, response.Code)
}) })

@ -27,6 +27,7 @@ import (
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
@ -118,7 +119,7 @@ func TestMiddlewareContext(t *testing.T) {
data := &dtos.IndexViewData{ data := &dtos.IndexViewData{
User: &dtos.CurrentUser{}, User: &dtos.CurrentUser{},
Settings: map[string]interface{}{}, Settings: map[string]interface{}{},
NavTree: []*dtos.NavLink{}, NavTree: []*navtree.NavLink{},
} }
t.Log("Calling HTML", "data", data) t.Log("Calling HTML", "data", data)
c.HTML(http.StatusOK, "index-template", data) c.HTML(http.StatusOK, "index-template", data)

@ -86,6 +86,7 @@ import (
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
"github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl" "github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl"
"github.com/grafana/grafana/pkg/services/navtree/navtreeimpl"
"github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert"
ngimage "github.com/grafana/grafana/pkg/services/ngalert/image" ngimage "github.com/grafana/grafana/pkg/services/ngalert/image"
ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics"
@ -351,6 +352,7 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(secretsMigrations.SecretMigrationProvider), new(*secretsMigrations.SecretMigrationProviderImpl)), wire.Bind(new(secretsMigrations.SecretMigrationProvider), new(*secretsMigrations.SecretMigrationProviderImpl)),
userauthimpl.ProvideService, userauthimpl.ProvideService,
acimpl.ProvideAccessControl, acimpl.ProvideAccessControl,
navtreeimpl.ProvideService,
wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)),
wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)),
tagimpl.ProvideService, tagimpl.ProvideService,

@ -306,6 +306,15 @@ const (
ActionUsersQuotasUpdate = "users.quotas:write" ActionUsersQuotasUpdate = "users.quotas:write"
// Org actions // Org actions
ActionOrgsRead = "orgs:read"
ActionOrgsPreferencesRead = "orgs.preferences:read"
ActionOrgsQuotasRead = "orgs.quotas:read"
ActionOrgsWrite = "orgs:write"
ActionOrgsPreferencesWrite = "orgs.preferences:write"
ActionOrgsQuotasWrite = "orgs.quotas:write"
ActionOrgsDelete = "orgs:delete"
ActionOrgsCreate = "orgs:create"
ActionOrgUsersRead = "org.users:read" ActionOrgUsersRead = "org.users:read"
ActionOrgUsersAdd = "org.users:add" ActionOrgUsersAdd = "org.users:add"
ActionOrgUsersRemove = "org.users:remove" ActionOrgUsersRemove = "org.users:remove"
@ -418,3 +427,53 @@ func BuiltInRolesWithParents(builtInRoles []string) map[string]struct{} {
return res return res
} }
// Evaluators
// TeamsAccessEvaluator is used to protect the "Configuration > Teams" page access
// grants access to a user when they can either create teams or can read and update a team
var TeamsAccessEvaluator = EvalAny(
EvalPermission(ActionTeamsCreate),
EvalAll(
EvalPermission(ActionTeamsRead),
EvalAny(
EvalPermission(ActionTeamsWrite),
EvalPermission(ActionTeamsPermissionsWrite),
),
),
)
// TeamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access
var TeamsEditAccessEvaluator = EvalAll(
EvalPermission(ActionTeamsRead),
EvalAny(
EvalPermission(ActionTeamsCreate),
EvalPermission(ActionTeamsWrite),
EvalPermission(ActionTeamsPermissionsWrite),
),
)
// OrgPreferencesAccessEvaluator is used to protect the "Configure > Preferences" page access
var OrgPreferencesAccessEvaluator = EvalAny(
EvalAll(
EvalPermission(ActionOrgsRead),
EvalPermission(ActionOrgsWrite),
),
EvalAll(
EvalPermission(ActionOrgsPreferencesRead),
EvalPermission(ActionOrgsPreferencesWrite),
),
)
// OrgsAccessEvaluator is used to protect the "Server Admin > Orgs" page access
// (you need to have read access to update or delete orgs; read is the minimum)
var OrgsAccessEvaluator = EvalPermission(ActionOrgsRead)
// OrgsCreateAccessEvaluator is used to protect the "Server Admin > Orgs > New Org" page access
var OrgsCreateAccessEvaluator = EvalAll(
EvalPermission(ActionOrgsRead),
EvalPermission(ActionOrgsCreate),
)
// ApiKeyAccessEvaluator is used to protect the "Configuration > API keys" page access
var ApiKeyAccessEvaluator = EvalPermission(ActionAPIKeyRead)

@ -4,6 +4,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -56,7 +57,7 @@ func ProvideService(cfg *setting.Cfg, hooksService *hooks.HooksService) *OSSLice
l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *models.ReqContext) { l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *models.ReqContext) {
for _, node := range indexData.NavTree { for _, node := range indexData.NavTree {
if node.Id == "admin" { if node.Id == "admin" {
node.Children = append(node.Children, &dtos.NavLink{ node.Children = append(node.Children, &navtree.NavLink{
Text: "Stats and license", Text: "Stats and license",
Id: "upgrading", Id: "upgrading",
Url: l.LicenseURL(req.IsGrafanaAdmin), Url: l.LicenseURL(req.IsGrafanaAdmin),

@ -0,0 +1,69 @@
package navtree
const (
// These weights may be used by an extension to reliably place
// itself in relation to a particular item in the menu. The weights
// are negative to ensure that the default items are placed above
// any items with default weight.
WeightSavedItems = (iota - 20) * 100
WeightCreate
WeightDashboard
WeightExplore
WeightAlerting
WeightDataConnections
WeightPlugin
WeightConfig
WeightAdmin
WeightProfile
WeightHelp
)
const (
NavSectionCore string = "core"
NavSectionPlugin string = "plugin"
NavSectionConfig string = "config"
)
type NavLink struct {
Id string `json:"id,omitempty"`
Text string `json:"text"`
Description string `json:"description,omitempty"`
Section string `json:"section,omitempty"`
SubTitle string `json:"subTitle,omitempty"`
Icon string `json:"icon,omitempty"` // Available icons can be browsed in Storybook: https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
SortWeight int64 `json:"sortWeight,omitempty"`
Divider bool `json:"divider,omitempty"`
HideFromMenu bool `json:"hideFromMenu,omitempty"`
HideFromTabs bool `json:"hideFromTabs,omitempty"`
ShowIconInNavbar bool `json:"showIconInNavbar,omitempty"`
RoundIcon bool `json:"roundIcon,omitempty"`
Children []*NavLink `json:"children,omitempty"`
HighlightText string `json:"highlightText,omitempty"`
HighlightID string `json:"highlightId,omitempty"`
EmptyMessageId string `json:"emptyMessageId,omitempty"`
}
// NavIDCfg is the id for org configuration navigation node
const NavIDCfg = "cfg"
func GetServerAdminNode(children []*NavLink) *NavLink {
url := ""
if len(children) > 0 {
url = children[0].Url
}
return &NavLink{
Text: "Server admin",
SubTitle: "Manage all users and orgs",
HideFromTabs: true,
Id: "admin",
Icon: "shield",
Url: url,
SortWeight: WeightAdmin,
Section: NavSectionConfig,
Children: children,
}
}

@ -0,0 +1,10 @@
package navtree
import (
"github.com/grafana/grafana/pkg/models"
pref "github.com/grafana/grafana/pkg/services/preference"
)
type Service interface {
GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) ([]*NavLink, error)
}

@ -0,0 +1,116 @@
package navtreeimpl
import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
)
func (s *ServiceImpl) setupConfigNodes(c *models.ReqContext) ([]*navtree.NavLink, error) {
var configNodes []*navtree.NavLink
hasAccess := ac.HasAccess(s.accessControl, c)
if hasAccess(ac.ReqOrgAdmin, datasources.ConfigurationPageAccess) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Data sources",
Icon: "database",
Description: "Add and configure data sources",
Id: "datasources",
Url: s.cfg.AppSubURL + "/datasources",
})
}
if s.features.IsEnabled(featuremgmt.FlagCorrelations) && hasAccess(ac.ReqOrgAdmin, correlations.ConfigurationPageAccess) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Correlations",
Icon: "gf-glue",
Description: "Add and configure correlations",
Id: "correlations",
Url: s.cfg.AppSubURL + "/datasources/correlations",
})
}
if hasAccess(ac.ReqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Users",
Id: "users",
Description: "Manage org members",
Icon: "user",
Url: s.cfg.AppSubURL + "/org/users",
})
}
if hasAccess(s.ReqCanAdminTeams, ac.TeamsAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Teams",
Id: "teams",
Description: "Manage org groups",
Icon: "users-alt",
Url: s.cfg.AppSubURL + "/org/teams",
})
}
// FIXME: while we don't have a permissions for listing plugins the legacy check has to stay as a default
if plugins.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(plugins.ReqCanAdminPlugins(s.cfg), plugins.AdminAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Plugins",
Id: "plugins",
Description: "View and configure plugins",
Icon: "plug",
Url: s.cfg.AppSubURL + "/plugins",
})
}
if hasAccess(ac.ReqOrgAdmin, ac.OrgPreferencesAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Preferences",
Id: "org-settings",
Description: "Organization preferences",
Icon: "sliders-v-alt",
Url: s.cfg.AppSubURL + "/org",
})
}
hideApiKeys, _, _ := s.kvStore.Get(c.Req.Context(), c.OrgID, "serviceaccounts", "hideApiKeys")
apiKeys, err := s.apiKeyService.GetAllAPIKeys(c.Req.Context(), c.OrgID)
if err != nil {
return nil, err
}
apiKeysHidden := hideApiKeys == "1" && len(apiKeys) == 0
if hasAccess(ac.ReqOrgAdmin, ac.ApiKeyAccessEvaluator) && !apiKeysHidden {
configNodes = append(configNodes, &navtree.NavLink{
Text: "API keys",
Id: "apikeys",
Description: "Create & manage API keys",
Icon: "key-skeleton-alt",
Url: s.cfg.AppSubURL + "/org/apikeys",
})
}
if enableServiceAccount(s, c) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Service accounts",
Id: "serviceaccounts",
Description: "Manage service accounts",
Icon: "gf-service-account",
Url: s.cfg.AppSubURL + "/org/serviceaccounts",
})
}
return configNodes, nil
}
func (s *ServiceImpl) ReqCanAdminTeams(c *models.ReqContext) bool {
return c.OrgRole == org.RoleAdmin || (s.cfg.EditorsCanAdmin && c.OrgRole == org.RoleEditor)
}
func enableServiceAccount(s *ServiceImpl, c *models.ReqContext) bool {
hasAccess := ac.HasAccess(s.accessControl, c)
return hasAccess(ac.ReqOrgAdmin, serviceaccounts.AccessEvaluator)
}

@ -0,0 +1,113 @@
package navtreeimpl
import (
"path"
"sort"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/pluginsettings"
)
func (s *ServiceImpl) getAppLinks(c *models.ReqContext) ([]*navtree.NavLink, error) {
hasAccess := ac.HasAccess(s.accessControl, c)
appLinks := []*navtree.NavLink{}
pss, err := s.pluginSettings.GetPluginSettings(c.Req.Context(), &pluginsettings.GetArgs{OrgID: c.OrgID})
if err != nil {
return nil, err
}
isPluginEnabled := func(plugin plugins.PluginDTO) bool {
if plugin.AutoEnabled {
return true
}
for _, ps := range pss {
if ps.PluginID == plugin.ID {
return ps.Enabled
}
}
return false
}
for _, plugin := range s.pluginStore.Plugins(c.Req.Context(), plugins.App) {
if !isPluginEnabled(plugin) {
continue
}
if !hasAccess(ac.ReqSignedIn,
ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) {
continue
}
appLink := &navtree.NavLink{
Text: plugin.Name,
Id: "plugin-page-" + plugin.ID,
Img: plugin.Info.Logos.Small,
Section: navtree.NavSectionPlugin,
SortWeight: navtree.WeightPlugin,
}
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
appLink.Url = s.cfg.AppSubURL + "/a/" + plugin.ID
} else {
appLink.Url = path.Join(s.cfg.AppSubURL, plugin.DefaultNavURL)
}
for _, include := range plugin.Includes {
if !c.HasUserRole(include.Role) {
continue
}
if include.Type == "page" && include.AddToNav {
var link *navtree.NavLink
if len(include.Path) > 0 {
link = &navtree.NavLink{
Url: s.cfg.AppSubURL + include.Path,
Text: include.Name,
}
if include.DefaultNav && !s.features.IsEnabled(featuremgmt.FlagTopnav) {
appLink.Url = link.Url // Overwrite the hardcoded page logic
}
} else {
link = &navtree.NavLink{
Url: s.cfg.AppSubURL + "/plugins/" + plugin.ID + "/page/" + include.Slug,
Text: include.Name,
}
}
link.Icon = include.Icon
appLink.Children = append(appLink.Children, link)
}
if include.Type == "dashboard" && include.AddToNav {
dboardURL := include.DashboardURLPath()
if dboardURL != "" {
link := &navtree.NavLink{
Url: path.Join(s.cfg.AppSubURL, dboardURL),
Text: include.Name,
}
appLink.Children = append(appLink.Children, link)
}
}
}
if len(appLink.Children) > 0 {
// If we only have one child and it's the app default nav then remove it from children
if len(appLink.Children) == 1 && appLink.Children[0].Url == appLink.Url {
appLink.Children = []*navtree.NavLink{}
}
appLinks = append(appLinks, appLink)
}
}
if len(appLinks) > 0 {
sort.SliceStable(appLinks, func(i, j int) bool {
return appLinks[i].Text < appLinks[j].Text
})
}
return appLinks, nil
}

@ -0,0 +1,598 @@
package navtreeimpl
import (
"fmt"
"sort"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsettings"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/setting"
)
type ServiceImpl struct {
cfg *setting.Cfg
log log.Logger
accessControl ac.AccessControl
pluginStore plugins.Store
pluginSettings pluginsettings.Service
starService star.Service
features *featuremgmt.FeatureManager
dashboardService dashboards.DashboardService
accesscontrolService ac.Service
kvStore kvstore.KVStore
apiKeyService apikey.Service
}
func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore plugins.Store, pluginSettings pluginsettings.Service, starService star.Service, features *featuremgmt.FeatureManager, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service) navtree.Service {
return &ServiceImpl{
cfg: cfg,
log: log.New("navtree service"),
accessControl: accessControl,
pluginStore: pluginStore,
pluginSettings: pluginSettings,
starService: starService,
features: features,
dashboardService: dashboardService,
accesscontrolService: accesscontrolService,
kvStore: kvStore,
apiKeyService: apiKeyService,
}
}
//nolint:gocyclo
func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) ([]*navtree.NavLink, error) {
hasAccess := ac.HasAccess(s.accessControl, c)
var navTree []*navtree.NavLink
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsRead)) {
starredItemsLinks, err := s.buildStarredItemsNavLinks(c, prefs)
if err != nil {
return nil, err
}
navTree = append(navTree, &navtree.NavLink{
Text: "Starred",
Id: "starred",
Icon: "star",
SortWeight: navtree.WeightSavedItems,
Section: navtree.NavSectionCore,
Children: starredItemsLinks,
EmptyMessageId: "starred-empty",
})
dashboardChildLinks := s.buildDashboardNavLinks(c, hasEditPerm)
dashboardsUrl := "/dashboards"
dashboardLink := &navtree.NavLink{
Text: "Dashboards",
Id: "dashboards",
SubTitle: "Manage dashboards and folders",
Icon: "apps",
Url: s.cfg.AppSubURL + dashboardsUrl,
SortWeight: navtree.WeightDashboard,
Section: navtree.NavSectionCore,
Children: dashboardChildLinks,
}
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
dashboardLink.Id = "dashboards/browse"
}
navTree = append(navTree, dashboardLink)
}
canExplore := func(context *models.ReqContext) bool {
return c.OrgRole == org.RoleAdmin || c.OrgRole == org.RoleEditor || setting.ViewersCanEdit
}
if setting.ExploreEnabled && hasAccess(canExplore, ac.EvalPermission(ac.ActionDatasourcesExplore)) {
navTree = append(navTree, &navtree.NavLink{
Text: "Explore",
Id: "explore",
SubTitle: "Explore your data",
Icon: "compass",
SortWeight: navtree.WeightExplore,
Section: navtree.NavSectionCore,
Url: s.cfg.AppSubURL + "/explore",
})
}
navTree = s.addProfile(navTree, c)
_, uaIsDisabledForOrg := s.cfg.UnifiedAlerting.DisabledOrgs[c.OrgID]
uaVisibleForOrg := s.cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg
if setting.AlertingEnabled != nil && *setting.AlertingEnabled {
navTree = append(navTree, s.buildLegacyAlertNavLinks(c)...)
} else if uaVisibleForOrg {
navTree = append(navTree, s.buildAlertNavLinks(c, hasEditPerm)...)
}
if s.features.IsEnabled(featuremgmt.FlagDataConnectionsConsole) {
navTree = append(navTree, s.buildDataConnectionsNavLink(c))
}
appLinks, err := s.getAppLinks(c)
if err != nil {
return nil, err
}
// When topnav is enabled we can test new information architecture where plugins live in Apps category
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
navTree = append(navTree, &navtree.NavLink{
Text: "Apps",
Icon: "apps",
Description: "App plugins",
Id: "apps",
Children: appLinks,
Section: navtree.NavSectionCore,
Url: s.cfg.AppSubURL + "/apps",
})
} else {
navTree = append(navTree, appLinks...)
}
configNodes, err := s.setupConfigNodes(c)
if err != nil {
return navTree, err
}
if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
liveNavLinks := []*navtree.NavLink{}
liveNavLinks = append(liveNavLinks, &navtree.NavLink{
Text: "Status", Id: "live-status", Url: s.cfg.AppSubURL + "/live", Icon: "exchange-alt",
})
liveNavLinks = append(liveNavLinks, &navtree.NavLink{
Text: "Pipeline", Id: "live-pipeline", Url: s.cfg.AppSubURL + "/live/pipeline", Icon: "arrow-to-right",
})
liveNavLinks = append(liveNavLinks, &navtree.NavLink{
Text: "Cloud", Id: "live-cloud", Url: s.cfg.AppSubURL + "/live/cloud", Icon: "cloud-upload",
})
navTree = append(navTree, &navtree.NavLink{
Id: "live",
Text: "Live",
SubTitle: "Event streaming",
Icon: "exchange-alt",
Url: s.cfg.AppSubURL + "/live",
Children: liveNavLinks,
Section: navtree.NavSectionConfig,
HideFromTabs: true,
})
}
var configNode *navtree.NavLink
var serverAdminNode *navtree.NavLink
if len(configNodes) > 0 {
configNode = &navtree.NavLink{
Id: navtree.NavIDCfg,
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
Icon: "cog",
Url: configNodes[0].Url,
Section: navtree.NavSectionConfig,
SortWeight: navtree.WeightConfig,
Children: configNodes,
}
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
configNode.Url = "/admin"
} else {
configNode.Url = configNodes[0].Url
}
navTree = append(navTree, configNode)
}
adminNavLinks := s.buildAdminNavLinks(c)
if len(adminNavLinks) > 0 {
serverAdminNode = navtree.GetServerAdminNode(adminNavLinks)
navTree = append(navTree, serverAdminNode)
}
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
// Move server admin into Configuration and rename to administration
if configNode != nil && serverAdminNode != nil {
configNode.Text = "Administration"
serverAdminNode.Url = "/admin/server"
serverAdminNode.HideFromTabs = false
configNode.Children = append(configNode.Children, serverAdminNode)
adminNodeIndex := len(navTree) - 1
navTree = navTree[:adminNodeIndex]
}
}
navTree = s.addHelpLinks(navTree, c)
return navTree, nil
}
func (s *ServiceImpl) addHelpLinks(navTree []*navtree.NavLink, c *models.ReqContext) []*navtree.NavLink {
if setting.HelpEnabled {
helpVersion := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit)
if s.cfg.AnonymousHideVersion && !c.IsSignedIn {
helpVersion = setting.ApplicationName
}
navTree = append(navTree, &navtree.NavLink{
Text: "Help",
SubTitle: helpVersion,
Id: "help",
Url: "#",
Icon: "question-circle",
SortWeight: navtree.WeightHelp,
Section: navtree.NavSectionConfig,
Children: []*navtree.NavLink{},
})
}
return navTree
}
func (s *ServiceImpl) addProfile(navTree []*navtree.NavLink, c *models.ReqContext) []*navtree.NavLink {
if setting.ProfileEnabled && c.IsSignedIn {
navTree = append(navTree, s.getProfileNode(c))
}
return navTree
}
func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink {
// Only set login if it's different from the name
var login string
if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
login = c.SignedInUser.Login
}
gravatarURL := dtos.GetGravatarUrl(c.Email)
children := []*navtree.NavLink{
{
Text: "Preferences", Id: "profile/settings", Url: s.cfg.AppSubURL + "/profile", Icon: "sliders-v-alt",
},
}
children = append(children, &navtree.NavLink{
Text: "Notification history", Id: "profile/notifications", Url: s.cfg.AppSubURL + "/profile/notifications", Icon: "bell",
})
if setting.AddChangePasswordLink() {
children = append(children, &navtree.NavLink{
Text: "Change password", Id: "profile/password", Url: s.cfg.AppSubURL + "/profile/password",
Icon: "lock",
})
}
if !setting.DisableSignoutMenu {
// add sign out first
children = append(children, &navtree.NavLink{
Text: "Sign out",
Id: "sign-out",
Url: s.cfg.AppSubURL + "/logout",
Icon: "arrow-from-right",
Target: "_self",
HideFromTabs: true,
})
}
return &navtree.NavLink{
Text: c.SignedInUser.NameOrFallback(),
SubTitle: login,
Id: "profile",
Img: gravatarURL,
Url: s.cfg.AppSubURL + "/profile",
Section: navtree.NavSectionConfig,
SortWeight: navtree.WeightProfile,
Children: children,
RoundIcon: true,
}
}
func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext, prefs *pref.Preference) ([]*navtree.NavLink, error) {
starredItemsChildNavs := []*navtree.NavLink{}
query := star.GetUserStarsQuery{
UserID: c.SignedInUser.UserID,
}
starredDashboardResult, err := s.starService.GetByUser(c.Req.Context(), &query)
if err != nil {
return nil, err
}
starredDashboards := []*models.Dashboard{}
starredDashboardsCounter := 0
for dashboardId := range starredDashboardResult.UserStars {
// Set a loose limit to the first 50 starred dashboards found
if starredDashboardsCounter > 50 {
break
}
starredDashboardsCounter++
query := &models.GetDashboardQuery{
Id: dashboardId,
OrgId: c.OrgID,
}
err := s.dashboardService.GetDashboard(c.Req.Context(), query)
if err == nil {
starredDashboards = append(starredDashboards, query.Result)
}
}
if len(starredDashboards) > 0 {
sort.Slice(starredDashboards, func(i, j int) bool {
return starredDashboards[i].Title < starredDashboards[j].Title
})
for _, starredItem := range starredDashboards {
starredItemsChildNavs = append(starredItemsChildNavs, &navtree.NavLink{
Id: starredItem.Uid,
Text: starredItem.Title,
Url: starredItem.GetUrl(),
})
}
}
return starredItemsChildNavs, nil
}
func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*navtree.NavLink {
hasAccess := ac.HasAccess(s.accessControl, c)
hasEditPermInAnyFolder := func(c *models.ReqContext) bool {
return hasEditPerm
}
dashboardChildNavs := []*navtree.NavLink{}
if !s.features.IsEnabled(featuremgmt.FlagTopnav) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Browse", Id: "dashboards/browse", Url: s.cfg.AppSubURL + "/dashboards", Icon: "sitemap",
})
}
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Playlists", Id: "dashboards/playlists", Url: s.cfg.AppSubURL + "/playlists", Icon: "presentation-play",
})
if c.IsSignedIn {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Snapshots",
Id: "dashboards/snapshots",
Url: s.cfg.AppSubURL + "/dashboard/snapshots",
Icon: "camera",
})
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Library panels",
Id: "dashboards/library-panels",
Url: s.cfg.AppSubURL + "/library-panels",
Icon: "library-panel",
})
}
if s.features.IsEnabled(featuremgmt.FlagScenes) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Scenes",
Id: "scenes",
Url: s.cfg.AppSubURL + "/scenes",
Icon: "apps",
})
}
if hasEditPerm {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
})
if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "New dashboard", Icon: "plus", Url: s.cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "dashboards/new", ShowIconInNavbar: true,
})
}
if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalPermission(dashboards.ActionFoldersCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "New folder", SubTitle: "Create a new folder to organize your dashboards", Id: "dashboards/folder/new",
Icon: "plus", Url: s.cfg.AppSubURL + "/dashboards/folder/new", HideFromTabs: true, ShowIconInNavbar: true,
})
}
if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "dashboards/import", Icon: "plus",
Url: s.cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, ShowIconInNavbar: true,
})
}
}
return dashboardChildNavs
}
func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) []*navtree.NavLink {
var alertChildNavs []*navtree.NavLink
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Alert rules", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
})
if c.HasRole(org.RoleEditor) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Notification channels", Id: "channels", Url: s.cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share",
})
}
var alertNav = navtree.NavLink{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting-legacy",
Icon: "bell",
Children: alertChildNavs,
Section: navtree.NavSectionCore,
SortWeight: navtree.WeightAlerting,
}
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
alertNav.Url = s.cfg.AppSubURL + "/alerting"
} else {
alertNav.Url = s.cfg.AppSubURL + "/alerting/list"
}
return []*navtree.NavLink{&alertNav}
}
func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) []*navtree.NavLink {
hasAccess := ac.HasAccess(s.accessControl, c)
var alertChildNavs []*navtree.NavLink
if hasAccess(ac.ReqViewer, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Alert rules", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
})
}
if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsRead), ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Contact points", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share", SubTitle: "Manage the settings of your contact points",
})
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Notification policies", Id: "am-routes", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
}
if hasAccess(ac.ReqViewer, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceRead), ac.EvalPermission(ac.ActionAlertingInstancesExternalRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Silences", Id: "silences", Url: s.cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
}
if c.OrgRole == org.RoleAdmin {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Admin", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin",
Icon: "cog",
})
}
fallbackHasEditPerm := func(*models.ReqContext) bool { return hasEditPerm }
if hasAccess(fallbackHasEditPerm, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingRuleExternalWrite))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
})
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "New alert rule", SubTitle: "Create an alert rule", Id: "alert",
Icon: "plus", Url: s.cfg.AppSubURL + "/alerting/new", HideFromTabs: true, ShowIconInNavbar: true,
})
}
if len(alertChildNavs) > 0 {
var alertNav = navtree.NavLink{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting",
Icon: "bell",
Children: alertChildNavs,
Section: navtree.NavSectionCore,
SortWeight: navtree.WeightAlerting,
}
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
alertNav.Url = s.cfg.AppSubURL + "/alerting"
} else {
alertNav.Url = s.cfg.AppSubURL + "/alerting/list"
}
return []*navtree.NavLink{&alertNav}
}
return nil
}
func (s *ServiceImpl) buildDataConnectionsNavLink(c *models.ReqContext) *navtree.NavLink {
var children []*navtree.NavLink
var navLink *navtree.NavLink
baseId := "data-connections"
baseUrl := s.cfg.AppSubURL + "/" + baseId
children = append(children, &navtree.NavLink{
Id: baseId + "-datasources",
Text: "Data sources",
Icon: "database",
Description: "Add and configure data sources",
Url: baseUrl + "/datasources",
})
children = append(children, &navtree.NavLink{
Id: baseId + "-plugins",
Text: "Plugins",
Icon: "plug",
Description: "Manage plugins",
Url: baseUrl + "/plugins",
})
children = append(children, &navtree.NavLink{
Id: baseId + "-cloud-integrations",
Text: "Cloud integrations",
Icon: "bolt",
Description: "Manage your cloud integrations",
Url: baseUrl + "/cloud-integrations",
})
navLink = &navtree.NavLink{
Text: "Data Connections",
Icon: "link",
Id: baseId,
Url: baseUrl,
Children: children,
Section: navtree.NavSectionCore,
SortWeight: navtree.WeightDataConnections,
}
return navLink
}
func (s *ServiceImpl) buildAdminNavLinks(c *models.ReqContext) []*navtree.NavLink {
hasAccess := ac.HasAccess(s.accessControl, c)
hasGlobalAccess := ac.HasGlobalAccess(s.accessControl, s.accesscontrolService, c)
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
adminNavLinks := []*navtree.NavLink{}
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)) {
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
Text: "Users", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user",
})
}
if hasGlobalAccess(ac.ReqGrafanaAdmin, orgsAccessEvaluator) {
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
Text: "Orgs", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
})
}
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) {
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
Text: "Settings", Id: "server-settings", Url: s.cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
})
}
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) && s.features.IsEnabled(featuremgmt.FlagStorage) {
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
Text: "Storage",
Id: "storage",
Description: "Manage file storage",
Icon: "cube",
Url: s.cfg.AppSubURL + "/admin/storage",
})
}
if s.cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) {
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
Text: "LDAP", Id: "ldap", Url: s.cfg.AppSubURL + "/admin/ldap", Icon: "book",
})
}
return adminNavLinks
}

@ -130,3 +130,9 @@ type Stats struct {
ServiceAccounts int64 `xorm:"serviceaccounts"` ServiceAccounts int64 `xorm:"serviceaccounts"`
Tokens int64 `xorm:"serviceaccount_tokens"` Tokens int64 `xorm:"serviceaccount_tokens"`
} }
// AccessEvaluator is used to protect the "Configuration > Service accounts" page access
var AccessEvaluator = accesscontrol.EvalAny(
accesscontrol.EvalPermission(ActionRead),
accesscontrol.EvalPermission(ActionCreate),
)

Loading…
Cancel
Save