Access Control: refactor permission evaluator to be more flexible (#35996)

* add a more flexible way to create permissions

* update interface for accesscontrol to use new eval interface

* use new eval interface

* update middleware to use new eval interface

* remove evaluator function and move metrics to service

* add tests for accesscontrol middleware

* Remove failed function from interface and update inejct to create a new
evaluator

* Change name

* Support Several sopes for a permission


* use evaluator and update fakeAccessControl

* Implement String that will return string representation of permissions
for an evaluator

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
Karl Persson 2021-08-24 11:36:28 +02:00 committed by GitHub
parent 9d8f61c738
commit 7ebf4027a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 869 additions and 349 deletions

View File

@ -2,13 +2,12 @@ package api
import (
"context"
"fmt"
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
)
@ -35,11 +34,11 @@ func (hs *HTTPServer) getAuthorizedSettings(ctx context.Context, user *models.Si
return bag, nil
}
eval := func(scopes ...string) (bool, error) {
return hs.AccessControl.Evaluate(ctx, user, accesscontrol.ActionSettingsRead, scopes...)
eval := func(scope string) (bool, error) {
return hs.AccessControl.Evaluate(ctx, user, ac.EvalPermission(ac.ActionSettingsRead, scope))
}
ok, err := eval(accesscontrol.ScopeSettingsAll)
ok, err := eval(ac.ScopeSettingsAll)
if err != nil {
return nil, err
}
@ -50,7 +49,7 @@ func (hs *HTTPServer) getAuthorizedSettings(ctx context.Context, user *models.Si
authorizedBag := make(setting.SettingsBag)
for section, keys := range bag {
ok, err := eval(getSettingsScope(section, "*"))
ok, err := eval(ac.Scope("settings", section, "*"))
if err != nil {
return nil, err
}
@ -60,7 +59,7 @@ func (hs *HTTPServer) getAuthorizedSettings(ctx context.Context, user *models.Si
}
for key := range keys {
ok, err := eval(getSettingsScope(section, key))
ok, err := eval(ac.Scope("settings", section, key))
if err != nil {
return nil, err
}
@ -74,7 +73,3 @@ func (hs *HTTPServer) getAuthorizedSettings(ctx context.Context, user *models.Si
}
return authorizedBag, nil
}
func getSettingsScope(section, key string) string {
return fmt.Sprintf("settings:%s:%s", section, key)
}

View File

@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
)
@ -55,23 +55,23 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/datasources/", reqOrgAdmin, hs.Index)
r.Get("/datasources/new", reqOrgAdmin, hs.Index)
r.Get("/datasources/edit/*", reqOrgAdmin, hs.Index)
r.Get("/org/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeUsersAll), hs.Index)
r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), hs.Index)
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, accesscontrol.ActionUsersCreate), hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index)
r.Get("/org/teams", reqCanAccessTeams, hs.Index)
r.Get("/org/teams/*", reqCanAccessTeams, hs.Index)
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
r.Get("/admin", reqGrafanaAdmin, hs.Index)
r.Get("/admin/settings", authorize(reqGrafanaAdmin, accesscontrol.ActionSettingsRead), hs.Index)
r.Get("/admin/users", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, accesscontrol.ScopeGlobalUsersAll), hs.Index)
r.Get("/admin/users/create", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersCreate), hs.Index)
r.Get("/admin/users/edit/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead), hs.Index)
r.Get("/admin/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), 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/edit/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead)), hs.Index)
r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index)
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
r.Get("/admin/stats", authorize(reqGrafanaAdmin, accesscontrol.ActionServerStatsRead), hs.Index)
r.Get("/admin/ldap", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPStatusRead), hs.Index)
r.Get("/admin/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
r.Get("/admin/ldap", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index)
r.Get("/styleguide", reqSignedIn, hs.Index)
@ -100,7 +100,7 @@ func (hs *HTTPServer) registerRoutes() {
f(c)
}
middleware.EnsureEditorOrViewerCanEdit(c)
}, accesscontrol.ActionDatasourcesExplore), hs.Index)
}, ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index)
r.Get("/playlists/", reqSignedIn, hs.Index)
r.Get("/playlists/*", reqSignedIn, hs.Index)
@ -163,16 +163,16 @@ func (hs *HTTPServer) registerRoutes() {
// users (admin permission required)
apiRoute.Group("/users", func(usersRoute routing.RouteRegister) {
const userIDScope = `global:users:{{ index . ":id" }}`
usersRoute.Get("/", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, accesscontrol.ScopeGlobalUsersAll), routing.Wrap(SearchUsers))
usersRoute.Get("/search", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, accesscontrol.ScopeGlobalUsersAll), routing.Wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, userIDScope), routing.Wrap(GetUserByID))
usersRoute.Get("/:id/teams", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersTeamRead, userIDScope), routing.Wrap(GetUserTeams))
usersRoute.Get("/:id/orgs", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, userIDScope), routing.Wrap(GetUserOrgList))
userIDScope := ac.Scope("global", "users", ac.Parameter(":id"))
usersRoute.Get("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(SearchUsers))
usersRoute.Get("/search", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(GetUserByID))
usersRoute.Get("/:id/teams", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersTeamRead, userIDScope)), routing.Wrap(GetUserTeams))
usersRoute.Get("/:id/orgs", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
usersRoute.Get("/lookup", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, accesscontrol.ScopeGlobalUsersAll), routing.Wrap(GetUserByLoginOrEmail))
usersRoute.Put("/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersWrite, userIDScope), bind(models.UpdateUserCommand{}), routing.Wrap(UpdateUser))
usersRoute.Post("/:id/using/:orgId", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersWrite, userIDScope), routing.Wrap(UpdateUserActiveOrg))
usersRoute.Get("/lookup", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(GetUserByLoginOrEmail))
usersRoute.Put("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), bind(models.UpdateUserCommand{}), routing.Wrap(UpdateUser))
usersRoute.Post("/:id/using/:orgId", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(UpdateUserActiveOrg))
})
// team (admin permission required)
@ -202,19 +202,19 @@ func (hs *HTTPServer) registerRoutes() {
// current org
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
const usersScope = `users:{{ index . ":userId" }}`
userIDScope := ac.Scope("users", ac.Parameter(":userId"))
orgRoute.Put("/", reqOrgAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrgCurrent))
orgRoute.Put("/address", reqOrgAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddressCurrent))
orgRoute.Get("/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeUsersAll), routing.Wrap(hs.GetOrgUsersForCurrentOrg))
orgRoute.Get("/users/search", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeUsersAll), routing.Wrap(hs.SearchOrgUsersWithPaging))
orgRoute.Post("/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeUsersAll), quota("user"), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUserToCurrentOrg))
orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRoleUpdate, usersScope), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRemove, usersScope), routing.Wrap(RemoveOrgUserForCurrentOrg))
orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsersForCurrentOrg))
orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.SearchOrgUsersWithPaging))
orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUserToCurrentOrg))
orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(RemoveOrgUserForCurrentOrg))
// invites
orgRoute.Get("/invites", authorize(reqOrgAdmin, accesscontrol.ActionUsersCreate), routing.Wrap(GetPendingOrgInvites))
orgRoute.Post("/invites", authorize(reqOrgAdmin, accesscontrol.ActionUsersCreate), quota("user"), bind(dtos.AddInviteForm{}), routing.Wrap(AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, accesscontrol.ActionUsersCreate), routing.Wrap(RevokeInvite))
orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(GetPendingOrgInvites))
orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), quota("user"), bind(dtos.AddInviteForm{}), routing.Wrap(AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(RevokeInvite))
// prefs
orgRoute.Get("/preferences", reqOrgAdmin, routing.Wrap(GetOrgPreferences))
@ -234,15 +234,15 @@ func (hs *HTTPServer) registerRoutes() {
// orgs (admin routes)
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
const usersScope = `users:{{ index . ":userId" }}`
userIDScope := ac.Scope("users", ac.Parameter(":userId"))
orgsRoute.Get("/", reqGrafanaAdmin, routing.Wrap(GetOrgByID))
orgsRoute.Put("/", reqGrafanaAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrg))
orgsRoute.Put("/address", reqGrafanaAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddress))
orgsRoute.Delete("/", reqGrafanaAdmin, routing.Wrap(DeleteOrgByID))
orgsRoute.Get("/users", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeUsersAll), routing.Wrap(hs.GetOrgUsers))
orgsRoute.Post("/users", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeUsersAll), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUser))
orgsRoute.Patch("/users/:userId", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersRoleUpdate, usersScope), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUser))
orgsRoute.Delete("/users/:userId", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersRemove, usersScope), routing.Wrap(RemoveOrgUser))
orgsRoute.Get("/users", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsers))
orgsRoute.Post("/users", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUser))
orgsRoute.Patch("/users/:userId", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUser))
orgsRoute.Delete("/users/:userId", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(RemoveOrgUser))
orgsRoute.Get("/quotas", reqGrafanaAdmin, routing.Wrap(GetOrgQuotas))
orgsRoute.Put("/quotas/:target", reqGrafanaAdmin, bind(models.UpdateOrgQuotaCmd{}), routing.Wrap(UpdateOrgQuota))
})
@ -439,36 +439,37 @@ func (hs *HTTPServer) registerRoutes() {
// admin api
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, accesscontrol.ActionSettingsRead), routing.Wrap(hs.AdminGetSettings))
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, accesscontrol.ActionServerStatsRead), routing.Wrap(AdminGetStats))
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, bind(dtos.PauseAllAlertsCommand{}), routing.Wrap(PauseAllAlerts))
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersDashboards), routing.Wrap(hs.AdminProvisioningReloadDashboards))
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersPlugins), routing.Wrap(hs.AdminProvisioningReloadPlugins))
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersDatasources), routing.Wrap(hs.AdminProvisioningReloadDatasources))
adminRoute.Post("/provisioning/notifications/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersNotifications), routing.Wrap(hs.AdminProvisioningReloadNotifications))
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards))
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources))
adminRoute.Post("/provisioning/notifications/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersNotifications)), routing.Wrap(hs.AdminProvisioningReloadNotifications))
adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPConfigReload), routing.Wrap(hs.ReloadLDAPCfg))
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPUsersSync), routing.Wrap(hs.PostSyncUserWithLDAP))
adminRoute.Get("/ldap/:username", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPUsersRead), routing.Wrap(hs.GetUserFromLDAP))
adminRoute.Get("/ldap/status", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPStatusRead), routing.Wrap(hs.GetLDAPStatus))
adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPConfigReload)), routing.Wrap(hs.ReloadLDAPCfg))
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(hs.PostSyncUserWithLDAP))
adminRoute.Get("/ldap/:username", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersRead)), routing.Wrap(hs.GetUserFromLDAP))
adminRoute.Get("/ldap/status", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), routing.Wrap(hs.GetLDAPStatus))
})
// Administering users
r.Group("/api/admin/users", func(adminUserRoute routing.RouteRegister) {
const userIDScope = `global:users:{{ index . ":id" }}`
adminUserRoute.Post("/", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersCreate), bind(dtos.AdminCreateUserForm{}), routing.Wrap(hs.AdminCreateUser))
adminUserRoute.Put("/:id/password", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersPasswordUpdate, userIDScope), bind(dtos.AdminUpdateUserPasswordForm{}), routing.Wrap(AdminUpdateUserPassword))
adminUserRoute.Put("/:id/permissions", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersPermissionsUpdate, userIDScope), bind(dtos.AdminUpdateUserPermissionsForm{}), routing.Wrap(hs.AdminUpdateUserPermissions))
adminUserRoute.Delete("/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersDelete, userIDScope), routing.Wrap(AdminDeleteUser))
adminUserRoute.Post("/:id/disable", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersDisable, userIDScope), routing.Wrap(hs.AdminDisableUser))
adminUserRoute.Post("/:id/enable", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersEnable, userIDScope), routing.Wrap(AdminEnableUser))
adminUserRoute.Get("/:id/quotas", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersQuotasList, userIDScope), routing.Wrap(GetUserQuotas))
adminUserRoute.Put("/:id/quotas/:target", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersQuotasUpdate, userIDScope), bind(models.UpdateUserQuotaCmd{}), routing.Wrap(UpdateUserQuota))
userIDScope := ac.Scope("global", "users", ac.Parameter(":id"))
adminUserRoute.Post("/:id/logout", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersLogout, userIDScope), routing.Wrap(hs.AdminLogoutUser))
adminUserRoute.Get("/:id/auth-tokens", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersAuthTokenList, userIDScope), routing.Wrap(hs.AdminGetUserAuthTokens))
adminUserRoute.Post("/:id/revoke-auth-token", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersAuthTokenUpdate, userIDScope), bind(models.RevokeAuthTokenCmd{}), routing.Wrap(hs.AdminRevokeUserAuthToken))
adminUserRoute.Post("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersCreate)), bind(dtos.AdminCreateUserForm{}), routing.Wrap(hs.AdminCreateUser))
adminUserRoute.Put("/:id/password", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersPasswordUpdate, userIDScope)), bind(dtos.AdminUpdateUserPasswordForm{}), routing.Wrap(AdminUpdateUserPassword))
adminUserRoute.Put("/:id/permissions", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersPermissionsUpdate, userIDScope)), bind(dtos.AdminUpdateUserPermissionsForm{}), routing.Wrap(hs.AdminUpdateUserPermissions))
adminUserRoute.Delete("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersDelete, userIDScope)), routing.Wrap(AdminDeleteUser))
adminUserRoute.Post("/:id/disable", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersDisable, userIDScope)), routing.Wrap(hs.AdminDisableUser))
adminUserRoute.Post("/:id/enable", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersEnable, userIDScope)), routing.Wrap(AdminEnableUser))
adminUserRoute.Get("/:id/quotas", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersQuotasList, userIDScope)), routing.Wrap(GetUserQuotas))
adminUserRoute.Put("/:id/quotas/:target", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersQuotasUpdate, userIDScope)), bind(models.UpdateUserQuotaCmd{}), routing.Wrap(UpdateUserQuota))
adminUserRoute.Post("/:id/logout", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
adminUserRoute.Get("/:id/auth-tokens", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
adminUserRoute.Post("/:id/revoke-auth-token", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), bind(models.RevokeAuthTokenCmd{}), routing.Wrap(hs.AdminRevokeUserAuthToken))
})
// rendering

View File

@ -8,9 +8,6 @@ import (
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/evaluator"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
@ -18,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/contexthandler"
@ -238,8 +236,12 @@ type fakeAccessControl struct {
permissions []*accesscontrol.Permission
}
func (f *fakeAccessControl) Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error) {
return evaluator.Evaluate(ctx, f, user, permission, scope...)
func (f *fakeAccessControl) Evaluate(ctx context.Context, user *models.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
permissions, err := f.GetUserPermissions(ctx, user)
if err != nil {
return false, err
}
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
}
func (f *fakeAccessControl) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) {

View File

@ -190,7 +190,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
return c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR || setting.ViewersCanEdit
}
if setting.ExploreEnabled && hasAccess(canExplore, ac.ActionDatasourcesExplore) {
if setting.ExploreEnabled && hasAccess(canExplore, ac.EvalPermission(ac.ActionDatasourcesExplore)) {
navTree = append(navTree, &dtos.NavLink{
Text: "Explore",
Id: "explore",
@ -263,7 +263,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
})
}
if hasAccess(ac.ReqOrgAdmin, ac.ActionOrgUsersRead, ac.ScopeUsersAll) {
if hasAccess(ac.ReqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Users",
Id: "users",
@ -358,7 +358,7 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
hasAccess := ac.HasAccess(hs.AccessControl, c)
adminNavLinks := []*dtos.NavLink{}
if hasAccess(ac.ReqGrafanaAdmin, ac.ActionUsersRead, ac.ScopeGlobalUsersAll) {
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",
})
@ -370,25 +370,25 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
})
}
if hasAccess(ac.ReqGrafanaAdmin, ac.ActionSettingsRead) {
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.ActionServerStatsRead) {
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Stats", Id: "server-stats", Url: hs.Cfg.AppSubURL + "/admin/stats", Icon: "graph-bar",
})
}
if hs.Cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.ActionLDAPStatusRead) {
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",
})
}
if hs.Cfg.PluginAdminEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.ActionPluginsManage) {
if hs.Cfg.PluginAdminEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionPluginsManage)) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Plugins", Id: "admin-plugins", Url: hs.Cfg.AppSubURL + "/admin/plugins", Icon: "plug",
})

View File

@ -8,13 +8,13 @@ import (
)
type AccessControl interface {
// Evaluate evaluates access to the given resource.
Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error)
// Evaluate evaluates access to the given resources.
Evaluate(ctx context.Context, user *models.SignedInUser, evaluator Evaluator) (bool, error)
// GetUserPermissions returns user permissions.
GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*Permission, error)
// Middleware checks if service disabled or not to switch to fallback authorization.
//IsDisabled returns if access control is enabled or not
IsDisabled() bool
// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their
@ -22,13 +22,13 @@ type AccessControl interface {
DeclareFixedRoles(...RoleRegistration) error
}
func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, permission string, scopes ...string) bool {
return func(fallback func(*models.ReqContext) bool, permission string, scopes ...string) bool {
func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool {
if ac.IsDisabled() {
return fallback(c)
}
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, permission, scopes...)
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
if err != nil {
c.Logger.Error("Error from access control system", "error", err)
return false
@ -55,6 +55,19 @@ func BuildPermissionsMap(permissions []*Permission) map[string]bool {
return permissionsMap
}
// GroupScopesByAction will group scopes on action
func GroupScopesByAction(permissions []*Permission) map[string]map[string]struct{} {
m := make(map[string]map[string]struct{})
for _, p := range permissions {
if _, ok := m[p.Action]; ok {
m[p.Action][p.Scope] = struct{}{}
} else {
m[p.Action] = map[string]struct{}{p.Scope: {}}
}
}
return m
}
func ValidateScope(scope string) bool {
prefix, last := scope[:len(scope)-1], scope[len(scope)-1]
// verify that last char is either ':' or '/' if last character of scope is '*'

View File

@ -0,0 +1,194 @@
package accesscontrol
import (
"bytes"
"fmt"
"html/template"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
)
var logger = log.New("accesscontrol.evaluator")
type Evaluator interface {
// Evaluate permissions that are grouped by action
Evaluate(permissions map[string]map[string]struct{}) (bool, error)
// Inject params into the evaluator's templated scopes. e.g. "settings:" + eval.Parameters(":id") and returns a new Evaluator
Inject(params map[string]string) (Evaluator, error)
// String returns a string representation of permission required by the evaluator
String() string
}
var _ Evaluator = new(permissionEvaluator)
// EvalPermission returns an evaluator that will require all scopes in combination with action to match
func EvalPermission(action string, scopes ...string) Evaluator {
return permissionEvaluator{Action: action, Scopes: scopes}
}
type permissionEvaluator struct {
Action string
Scopes []string
}
func (p permissionEvaluator) Evaluate(permissions map[string]map[string]struct{}) (bool, error) {
userScopes, ok := permissions[p.Action]
if !ok {
return false, nil
}
if len(p.Scopes) == 0 {
return true, nil
}
for _, target := range p.Scopes {
var err error
var matches bool
for scope := range userScopes {
matches, err = match(scope, target)
if err != nil {
return false, err
}
if matches {
break
}
}
if !matches {
return false, nil
}
}
return true, nil
}
func match(scope, target string) (bool, error) {
if scope == "" {
return false, nil
}
if !ValidateScope(scope) {
logger.Error(
"invalid scope",
"scope", scope,
"reason", "scopes should not contain meta-characters like * or ?, except in the last position",
)
return false, nil
}
prefix, last := scope[:len(scope)-1], scope[len(scope)-1]
//Prefix match
if last == '*' {
if strings.HasPrefix(target, prefix) {
logger.Debug("matched scope", "user scope", scope, "target scope", target)
return true, nil
}
}
return scope == target, nil
}
func (p permissionEvaluator) Inject(params map[string]string) (Evaluator, error) {
scopes := make([]string, 0, len(p.Scopes))
for _, scope := range p.Scopes {
tmpl, err := template.New("scope").Parse(scope)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = tmpl.Execute(&buf, params); err != nil {
return nil, err
}
scopes = append(scopes, buf.String())
}
return EvalPermission(p.Action, scopes...), nil
}
func (p permissionEvaluator) String() string {
return fmt.Sprintf("action:%s scopes:%s", p.Action, strings.Join(p.Scopes, ", "))
}
var _ Evaluator = new(allEvaluator)
// EvalAll returns evaluator that requires all passed evaluators to evaluate to true
func EvalAll(allOf ...Evaluator) Evaluator {
return allEvaluator{allOf: allOf}
}
type allEvaluator struct {
allOf []Evaluator
}
func (a allEvaluator) Evaluate(permissions map[string]map[string]struct{}) (bool, error) {
for _, e := range a.allOf {
if ok, err := e.Evaluate(permissions); !ok || err != nil {
return false, err
}
}
return true, nil
}
func (a allEvaluator) Inject(params map[string]string) (Evaluator, error) {
var injected []Evaluator
for _, e := range a.allOf {
i, err := e.Inject(params)
if err != nil {
return nil, err
}
injected = append(injected, i)
}
return EvalAll(injected...), nil
}
func (a allEvaluator) String() string {
permissions := make([]string, 0, len(a.allOf))
for _, e := range a.allOf {
permissions = append(permissions, e.String())
}
return fmt.Sprintf("all(%s)", strings.Join(permissions, " "))
}
var _ Evaluator = new(anyEvaluator)
// EvalAny returns evaluator that requires at least one of passed evaluators to evaluate to true
func EvalAny(anyOf ...Evaluator) Evaluator {
return anyEvaluator{anyOf: anyOf}
}
type anyEvaluator struct {
anyOf []Evaluator
}
func (a anyEvaluator) Evaluate(permissions map[string]map[string]struct{}) (bool, error) {
for _, e := range a.anyOf {
ok, err := e.Evaluate(permissions)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
}
func (a anyEvaluator) Inject(params map[string]string) (Evaluator, error) {
var injected []Evaluator
for _, e := range a.anyOf {
i, err := e.Inject(params)
if err != nil {
return nil, err
}
injected = append(injected, i)
}
return EvalAny(injected...), nil
}
func (a anyEvaluator) String() string {
permissions := make([]string, 0, len(a.anyOf))
for _, e := range a.anyOf {
permissions = append(permissions, e.String())
}
return fmt.Sprintf("any(%s)", strings.Join(permissions, " "))
}

View File

@ -1,101 +0,0 @@
package evaluator
import (
"context"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/prometheus/client_golang/prometheus"
)
var logger = log.New("accesscontrol.evaluator")
// Evaluate evaluates access to the given resource, using provided AccessControl instance.
// Scopes are evaluated with an `OR` relationship.
func Evaluate(ctx context.Context, ac accesscontrol.AccessControl, user *models.SignedInUser, action string, scope ...string) (bool, error) {
timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary)
defer timer.ObserveDuration()
metrics.MAccessEvaluationCount.Inc()
userPermissions, err := ac.GetUserPermissions(ctx, user)
if err != nil {
return false, err
}
ok, dbScopes := extractScopes(userPermissions, action)
if !ok {
return false, nil
}
res, err := evaluateScope(dbScopes, scope...)
return res, err
}
func evaluateScope(dbScopes map[string]struct{}, targetScopes ...string) (bool, error) {
if len(targetScopes) == 0 {
return true, nil
}
for _, s := range targetScopes {
for dbScope := range dbScopes {
if dbScope == "" {
continue
}
if !accesscontrol.ValidateScope(dbScope) {
logger.Error(
"invalid scope",
"reason", fmt.Sprintf("%v should not contain meta-characters like * or ?, except in the last position", dbScope),
"scope", dbScope,
)
continue
}
prefix, last := dbScope[:len(dbScope)-1], dbScope[len(dbScope)-1]
//Prefix match
if last == '*' {
if strings.HasPrefix(s, prefix) {
logger.Debug(
"matched scope",
"reason", fmt.Sprintf("matched request scope %v against resource scope %v", dbScope, s),
"request scope", dbScope,
"resource scope", s,
)
return true, nil
}
}
if s == dbScope {
return true, nil
}
}
}
logger.Debug(
"access control failed",
"request scope", dbScopes,
"resource scope", targetScopes,
"reason", fmt.Sprintf("Could not match resource scopes %v with request scopes %v", dbScopes, targetScopes),
)
return false, nil
}
func extractScopes(permissions []*accesscontrol.Permission, targetAction string) (bool, map[string]struct{}) {
scopes := map[string]struct{}{}
ok := false
for _, p := range permissions {
if p == nil {
continue
}
if p.Action == targetAction {
ok = true
scopes[p.Scope] = struct{}{}
}
}
return ok, scopes
}

View File

@ -1,112 +0,0 @@
package evaluator
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
func TestExtractPermission(t *testing.T) {
const targetPermission = "permissions:create"
userPermissions := []*accesscontrol.Permission{
{
Action: "permissions:create",
Scope: "teams:*",
},
{
Action: "permissions:create",
Scope: "permissions:*",
},
{
Action: "permissions:remove",
Scope: "permissions:*",
},
}
expectedScopes := map[string]struct{}{
"permissions:*": {},
"teams:*": {},
}
ok, scopes := extractScopes(userPermissions, targetPermission)
assert.True(t, ok)
assert.Equal(t, expectedScopes, scopes)
}
func TestEvaluatePermissions(t *testing.T) {
tests := []struct {
Name string
HasScopes map[string]struct{}
NeedAnyScope []string
Valid bool
}{
{
Name: "Base",
HasScopes: map[string]struct{}{},
NeedAnyScope: []string{},
Valid: true,
},
{
Name: "No expected scope always returns true",
HasScopes: map[string]struct{}{
"teams:*": {},
"permissions:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{},
Valid: true,
},
{
Name: "Single scope from list",
HasScopes: map[string]struct{}{
"teams:1": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"teams:1", "permissions:delegate"},
Valid: true,
},
{
Name: "Single scope from glob list",
HasScopes: map[string]struct{}{
"teams:*": {},
"permissions:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"teams:1", "permissions:delegate"},
Valid: true,
},
{
Name: "Either of two scopes from glob list",
HasScopes: map[string]struct{}{
"teams:*": {},
"permissions:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"global:admin", "permissions:delegate"},
Valid: true,
},
{
Name: "No match found",
HasScopes: map[string]struct{}{
"teams:*": {},
"users:*": {},
"permissions:delegate": {},
},
NeedAnyScope: []string{"teams1", "permissions:nodelegate"},
Valid: false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
ok, err := evaluateScope(tc.HasScopes, tc.NeedAnyScope...)
require.NoError(t, err)
assert.Equal(t, tc.Valid, ok)
})
}
}

View File

@ -0,0 +1,399 @@
package accesscontrol
import (
"testing"
"github.com/stretchr/testify/assert"
)
type evaluateTestCase struct {
desc string
expected bool
evaluator Evaluator
permissions map[string]map[string]struct{}
}
type injectTestCase struct {
desc string
expected bool
evaluator Evaluator
params map[string]string
permissions map[string]map[string]struct{}
}
func TestPermission_Evaluate(t *testing.T) {
tests := []evaluateTestCase{
{
desc: "should evaluate to true",
expected: true,
evaluator: EvalPermission("reports:read", "reports:1"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should evaluate to true when allEvaluator required scopes matches",
expected: true,
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
"reports:2": struct{}{},
},
},
},
{
desc: "should evaluate to true for empty scope",
expected: true,
evaluator: EvalPermission("reports:read"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should evaluate to false when only one of required scopes exists",
expected: false,
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestPermission_Inject(t *testing.T) {
tests := []injectTestCase{
{
desc: "should inject correct param",
expected: true,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
params: map[string]string{
":id": "10",
":reportId": "1",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should fail for nil params",
expected: false,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
params: nil,
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
},
},
{
desc: "should inject several parameters to one permission",
expected: true,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"), Parameter(":reportId2"))),
params: map[string]string{
":reportId": "report",
":reportId2": "report2",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:report:report2": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.Inject(test.params)
assert.NoError(t, err)
ok, err := injected.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAll_Evaluate(t *testing.T) {
tests := []evaluateTestCase{
{
desc: "should return true for one that matches",
evaluator: EvalAll(
EvalPermission("settings:write", Scope("settings", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return true for several that matches",
evaluator: EvalAll(
EvalPermission("settings:write", Scope("settings", "*")),
EvalPermission("settings:read", Scope("settings", "auth.saml", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
"settings:read": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return false if one does not match",
evaluator: EvalAll(
EvalPermission("settings:write", Scope("settings", "*")),
EvalPermission("settings:read", Scope("settings", "auth.saml", "*")),
EvalPermission("report:read", Scope("reports", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
"settings:read": {"settings:*": struct{}{}},
"report:read": {"report:1": struct{}{}},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAll_Inject(t *testing.T) {
tests := []injectTestCase{
{
desc: "should inject correct param",
expected: true,
evaluator: EvalAll(
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))),
),
params: map[string]string{
":id": "10",
":settingsId": "3",
":reportId": "1",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
{
desc: "should fail for nil params",
expected: false,
evaluator: EvalAll(
EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))),
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
),
params: nil,
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.Inject(test.params)
assert.NoError(t, err)
ok, err := injected.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAny_Evaluate(t *testing.T) {
tests := []evaluateTestCase{
{
desc: "should return true for one that matches",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return true when at least one matches",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "auth.saml", "*")),
EvalPermission("report:read", Scope("reports", "1")),
EvalPermission("report:write", Scope("reports", "10")),
),
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
expected: true,
},
{
desc: "should return false when there is no match",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "auth.saml", "*")),
EvalPermission("report:read", Scope("reports", "1")),
EvalPermission("report:write", Scope("reports", "10")),
),
permissions: map[string]map[string]struct{}{
"permissions:write": {"permissions:delegate": struct{}{}},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
func TestAny_Inject(t *testing.T) {
tests := []injectTestCase{
{
desc: "should inject correct param",
expected: true,
evaluator: EvalAny(
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))),
),
params: map[string]string{
":id": "10",
":settingsId": "3",
":reportId": "1",
},
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
{
desc: "should fail for nil params",
expected: false,
evaluator: EvalAny(
EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))),
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
),
params: nil,
permissions: map[string]map[string]struct{}{
"reports:read": {
"reports:1": struct{}{},
},
"settings:read": {
"settings:3": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.Inject(test.params)
assert.NoError(t, err)
ok, err := injected.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}
type combinedTestCase struct {
desc string
evaluator Evaluator
expected bool
permissions map[string]map[string]struct{}
}
func TestEval(t *testing.T) {
tests := []combinedTestCase{
{
desc: "should return true when first is true",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
EvalAll(
EvalPermission("settings:write", "settings:auth.saml:enabled"),
EvalPermission("settings:write", "settings:auth.saml:max_issue_delay"),
),
),
expected: true,
permissions: map[string]map[string]struct{}{
"settings:write": {"settings:*": struct{}{}},
},
},
{
desc: "should return true when first is false and all is true",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
EvalAll(
EvalPermission("settings:write", "settings:auth.saml:enabled"),
EvalPermission("settings:write", "settings:auth.saml:max_issue_delay"),
),
),
expected: true,
permissions: map[string]map[string]struct{}{
"settings:write": {
"settings:auth.saml:enabled": struct{}{},
"settings:auth.saml:max_issue_delay": struct{}{},
},
},
},
{
desc: "should return false when both are false",
evaluator: EvalAny(
EvalPermission("settings:write", Scope("settings", "*")),
EvalAll(
EvalPermission("settings:write", "settings:auth.saml:enabled"),
EvalPermission("settings:write", "settings:auth.saml:max_issue_delay"),
),
),
expected: false,
permissions: map[string]map[string]struct{}{
"settings:write": {
"settings:auth.saml:enabled": struct{}{},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ok, err := test.evaluator.Evaluate(test.permissions)
assert.NoError(t, err)
assert.Equal(t, test.expected, ok)
})
}
}

View File

@ -1,68 +1,50 @@
package middleware
import (
"bytes"
"fmt"
"net/http"
"text/template"
"time"
"github.com/grafana/grafana/pkg/util"
macaron "gopkg.in/macaron.v1"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/util"
)
func Middleware(ac accesscontrol.AccessControl) func(macaron.Handler, string, ...string) macaron.Handler {
return func(fallback macaron.Handler, permission string, scopes ...string) macaron.Handler {
func Middleware(ac accesscontrol.AccessControl) func(macaron.Handler, accesscontrol.Evaluator) macaron.Handler {
return func(fallback macaron.Handler, evaluator accesscontrol.Evaluator) macaron.Handler {
if ac.IsDisabled() {
return fallback
}
return func(c *models.ReqContext) {
// We need this otherwise templated scopes get initialized only once, during the first call
runtimeScope := make([]string, len(scopes))
for i, scope := range scopes {
var buf bytes.Buffer
tmpl, err := template.New("scope").Parse(scope)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err)
return
}
err = tmpl.Execute(&buf, c.AllParams())
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err)
return
}
runtimeScope[i] = buf.String()
}
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, permission, runtimeScope...)
injected, err := evaluator.Inject(c.AllParams())
if err != nil {
Deny(c, permission, runtimeScope, err)
c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err)
return
}
if !hasAccess {
Deny(c, permission, runtimeScope, nil)
hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, injected)
if !hasAccess || err != nil {
Deny(c, injected, err)
return
}
}
}
}
func Deny(c *models.ReqContext, permission string, scopes []string, err error) {
func Deny(c *models.ReqContext, evaluator accesscontrol.Evaluator, err error) {
id := newID()
if err != nil {
c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id)
} else {
c.Logger.Info("Access denied",
c.Logger.Info(
"Access denied",
"userID", c.UserId,
"permission", permission,
"scopes", scopes,
"accessErrorID", id)
"accessErrorID", id,
"permissions", evaluator.String(),
)
}
// If the user triggers an error in the access control system, we

View File

@ -0,0 +1,121 @@
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"gopkg.in/macaron.v1"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
type middlewareTestCase struct {
desc string
expectFallback bool
expectEndpoint bool
evaluator accesscontrol.Evaluator
ac accesscontrol.AccessControl
}
func TestMiddleware(t *testing.T) {
tests := []middlewareTestCase{
{
desc: "should use fallback if access control is disabled",
ac: fakeAccessControl{isDisabled: true},
expectFallback: true,
expectEndpoint: true,
},
{
desc: "should pass middleware for correct permissions",
ac: fakeAccessControl{
isDisabled: false,
permissions: []*accesscontrol.Permission{{Action: "users:read", Scope: "users:*"}},
},
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
expectFallback: false,
expectEndpoint: true,
},
{
desc: "should not reach endpoint when missing permissions",
ac: fakeAccessControl{
isDisabled: false,
permissions: []*accesscontrol.Permission{{Action: "users:read", Scope: "users:1"}},
},
evaluator: accesscontrol.EvalPermission("users:read", "users:*"),
expectFallback: false,
expectEndpoint: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
fallbackCalled := false
fallback := func(c *models.ReqContext) {
fallbackCalled = true
}
server := macaron.New()
server.UseMiddleware(macaron.Renderer("../../public/views", "[[", "]]"))
server.Use(contextProvider())
server.Use(Middleware(test.ac)(fallback, test.evaluator))
endpointCalled := false
server.Get("/", func(c *models.ReqContext) {
endpointCalled = true
})
request, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err)
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, request)
assert.Equal(t, test.expectFallback, fallbackCalled)
assert.Equal(t, test.expectEndpoint, endpointCalled)
})
}
}
func contextProvider() macaron.Handler {
return func(c *macaron.Context) {
reqCtx := &models.ReqContext{
Context: c,
Logger: log.New(""),
SignedInUser: &models.SignedInUser{},
IsSignedIn: true,
SkipCache: true,
}
c.Map(reqCtx)
}
}
var _ accesscontrol.AccessControl = new(fakeAccessControl)
type fakeAccessControl struct {
isDisabled bool
permissions []*accesscontrol.Permission
}
func (f fakeAccessControl) Evaluate(ctx context.Context, user *models.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
permissions, _ := f.GetUserPermissions(ctx, user)
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
}
func (f fakeAccessControl) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) {
return f.permissions, nil
}
func (f fakeAccessControl) IsDisabled() bool {
return f.isDisabled
}
func (f fakeAccessControl) DeclareFixedRoles(registration ...accesscontrol.RoleRegistration) error {
return nil
}

View File

@ -34,11 +34,6 @@ type Permission struct {
Scope string `json:"scope"`
}
type EvaluationResult struct {
HasAccess bool
Meta interface{}
}
func (p RoleDTO) Role() Role {
return Role{
Name: p.Name,
@ -53,13 +48,13 @@ const (
ActionUsersRead = "users:read"
ActionUsersWrite = "users:write"
ActionUsersTeamRead = "users.teams:read"
// We can ignore gosec G101 since this does not contain any credentials
// We can ignore gosec G101 since this does not contain any credentials.
// nolint:gosec
ActionUsersAuthTokenList = "users.authtoken:list"
// We can ignore gosec G101 since this does not contain any credentials
// We can ignore gosec G101 since this does not contain any credentials.
// nolint:gosec
ActionUsersAuthTokenUpdate = "users.authtoken:update"
// We can ignore gosec G101 since this does not contain any credentials
// We can ignore gosec G101 since this does not contain any credentials.
// nolint:gosec
ActionUsersPasswordUpdate = "users.password:update"
ActionUsersDelete = "users:delete"

View File

@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/evaluator"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
@ -55,9 +54,17 @@ func (ac *OSSAccessControlService) getUsageMetrics() interface{} {
return 1
}
// Evaluate evaluates access to the given resource
func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.SignedInUser, permission string, scope ...string) (bool, error) {
return evaluator.Evaluate(ctx, ac, user, permission, scope...)
// Evaluate evaluates access to the given resources
func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary)
defer timer.ObserveDuration()
metrics.MAccessEvaluationCount.Inc()
permissions, err := ac.GetUserPermissions(ctx, user)
if err != nil {
return false, err
}
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
}
// GetUserPermissions returns user permissions based on built-in roles

View File

@ -94,8 +94,7 @@ type userTestCase struct {
}
type endpointTestCase struct {
permission string
scope []string
evaluator accesscontrol.Evaluator
}
func TestEvaluatingPermissions(t *testing.T) {
@ -108,8 +107,8 @@ func TestEvaluatingPermissions(t *testing.T) {
isGrafanaAdmin: false,
},
endpoints: []endpointTestCase{
{permission: accesscontrol.ActionUsersDisable, scope: []string{accesscontrol.ScopeGlobalUsersAll}},
{permission: accesscontrol.ActionUsersEnable, scope: []string{accesscontrol.ScopeGlobalUsersAll}},
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersDisable, accesscontrol.ScopeGlobalUsersAll)},
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersEnable, accesscontrol.ScopeGlobalUsersAll)},
},
evalResult: true,
},
@ -121,7 +120,7 @@ func TestEvaluatingPermissions(t *testing.T) {
isGrafanaAdmin: false,
},
endpoints: []endpointTestCase{
{permission: accesscontrol.ActionUsersCreate, scope: []string{accesscontrol.ScopeGlobalUsersAll}},
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersCreate, accesscontrol.ScopeGlobalUsersAll)},
},
evalResult: false,
},
@ -140,7 +139,7 @@ func TestEvaluatingPermissions(t *testing.T) {
}
for _, endpoint := range tc.endpoints {
result, err := ac.Evaluate(context.Background(), user, endpoint.permission, endpoint.scope...)
result, err := ac.Evaluate(context.Background(), user, endpoint.evaluator)
require.NoError(t, err)
assert.Equal(t, tc.evalResult, result)
}

View File

@ -0,0 +1,25 @@
package accesscontrol
import (
"fmt"
"strings"
)
// Scope builds scope from parts
// e.g. Scope("users", "*") return "users:*"
func Scope(parts ...string) string {
b := strings.Builder{}
for i, c := range parts {
if i != 0 {
b.WriteRune(':')
}
b.WriteString(c)
}
return b.String()
}
// Parameter returns injectable scope part
// e.g. Scope("users", Parameter(":id")) or "users:" + Parameter(":id")
func Parameter(key string) string {
return fmt.Sprintf(`{{ index . "%s" }}`, key)
}