mirror of https://github.com/grafana/grafana.git
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:
parent
9d8f61c738
commit
7ebf4027a7
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
111
pkg/api/api.go
111
pkg/api/api.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 '*'
|
||||
|
|
|
|||
|
|
@ -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, " "))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue