mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AccessControl: Resource permission translator (#95423)
* Standardize Resource Translator in options * Add resource UID translator for resource permissions * fix comments * fix comments
This commit is contained in:
parent
fb51a5e21f
commit
2b867d9850
@ -283,7 +283,7 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)),
|
||||
datasourceservice.ProvideLegacyDataSourceLookup,
|
||||
serviceaccountsretriever.ProvideService,
|
||||
wire.Bind(new(serviceaccountsretriever.ServiceAccountRetriever), new(*serviceaccountsretriever.Service)),
|
||||
wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*serviceaccountsretriever.Service)),
|
||||
ossaccesscontrol.ProvideServiceAccountPermissions,
|
||||
wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)),
|
||||
serviceaccountsmanager.ProvideServiceAccountsService,
|
||||
|
@ -42,6 +42,9 @@ func ProvideReceiverPermissionsService(
|
||||
options := resourcepermissions.Options{
|
||||
Resource: "receivers",
|
||||
ResourceAttribute: "uid",
|
||||
ResourceTranslator: func(ctx context.Context, orgID int64, resourceID string) (string, error) {
|
||||
return alertingac.ScopeReceiversProvider.GetResourceIDFromUID(resourceID), nil
|
||||
},
|
||||
Assignments: resourcepermissions.Assignments{
|
||||
Users: true,
|
||||
Teams: true,
|
||||
|
@ -41,8 +41,9 @@ func ProvideServiceAccountPermissions(
|
||||
teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService,
|
||||
) (*ServiceAccountPermissionsService, error) {
|
||||
options := resourcepermissions.Options{
|
||||
Resource: "serviceaccounts",
|
||||
ResourceAttribute: "id",
|
||||
Resource: "serviceaccounts",
|
||||
ResourceAttribute: "id",
|
||||
ResourceTranslator: serviceaccounts.UIDToIDHandler(serviceAccountRetrieverService),
|
||||
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.ossaccesscontrol.ProvideServiceAccountPermissions.ResourceValidator")
|
||||
defer span.End()
|
||||
|
@ -41,9 +41,10 @@ func ProvideTeamPermissions(
|
||||
teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService,
|
||||
) (*TeamPermissionsService, error) {
|
||||
options := resourcepermissions.Options{
|
||||
Resource: "teams",
|
||||
ResourceAttribute: "id",
|
||||
OnlyManaged: true,
|
||||
Resource: "teams",
|
||||
ResourceAttribute: "id",
|
||||
OnlyManaged: true,
|
||||
ResourceTranslator: team.UIDToIDHandler(teamService),
|
||||
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.ossaccesscontrol.ProvideTeamerPermissions.ResourceValidator")
|
||||
defer span.End()
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -46,29 +45,40 @@ func (a *api) registerEndpoints() {
|
||||
}
|
||||
|
||||
teamUIDResolver := team.MiddlewareTeamUIDResolver(a.service.teamService, ":teamID")
|
||||
teamUIDResolverResource := func() web.Handler { return func(c *contextmodel.ReqContext) {} }() // no-op
|
||||
if a.service.options.Resource == "teams" {
|
||||
teamUIDResolverResource = team.MiddlewareTeamUIDResolver(a.service.teamService, ":resourceID")
|
||||
}
|
||||
if a.service.options.Resource == "receivers" {
|
||||
teamUIDResolverResource = MiddlewareReceiverUIDResolver(":resourceID")
|
||||
}
|
||||
resourceResolver := func(resTranslator ResourceTranslator) web.Handler {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
// no-op
|
||||
if resTranslator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gotParams := web.Params(c.Req)
|
||||
resourceID := gotParams[":resourceID"]
|
||||
resourceID, err := resTranslator(c.Req.Context(), c.OrgID, resourceID)
|
||||
if err == nil {
|
||||
gotParams[":resourceID"] = resourceID
|
||||
web.SetURLParams(c.Req, gotParams)
|
||||
} else {
|
||||
c.JsonApiErr(http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
}
|
||||
}(a.service.options.ResourceTranslator)
|
||||
|
||||
a.router.Group(fmt.Sprintf("/api/access-control/%s", a.service.options.Resource), func(r routing.RouteRegister) {
|
||||
actionRead := fmt.Sprintf("%s.permissions:read", a.service.options.Resource)
|
||||
actionWrite := fmt.Sprintf("%s.permissions:write", a.service.options.Resource)
|
||||
scope := accesscontrol.Scope(a.service.options.Resource, a.service.options.ResourceAttribute, accesscontrol.Parameter(":resourceID"))
|
||||
r.Get("/description", auth(accesscontrol.EvalPermission(actionRead)), routing.Wrap(a.getDescription))
|
||||
r.Get("/:resourceID", teamUIDResolverResource, auth(accesscontrol.EvalPermission(actionRead, scope)), routing.Wrap(a.getPermissions))
|
||||
r.Post("/:resourceID", teamUIDResolverResource, licenseMW, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setPermissions))
|
||||
r.Get("/:resourceID", resourceResolver, auth(accesscontrol.EvalPermission(actionRead, scope)), routing.Wrap(a.getPermissions))
|
||||
r.Post("/:resourceID", resourceResolver, licenseMW, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setPermissions))
|
||||
if a.service.options.Assignments.Users {
|
||||
r.Post("/:resourceID/users/:userID", licenseMW, teamUIDResolverResource, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setUserPermission))
|
||||
r.Post("/:resourceID/users/:userID", licenseMW, resourceResolver, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setUserPermission))
|
||||
}
|
||||
if a.service.options.Assignments.Teams {
|
||||
r.Post("/:resourceID/teams/:teamID", licenseMW, teamUIDResolverResource, teamUIDResolver, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setTeamPermission))
|
||||
r.Post("/:resourceID/teams/:teamID", licenseMW, resourceResolver, teamUIDResolver, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setTeamPermission))
|
||||
}
|
||||
if a.service.options.Assignments.BuiltInRoles {
|
||||
r.Post("/:resourceID/builtInRoles/:builtInRole", teamUIDResolverResource, licenseMW, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setBuiltinRolePermission))
|
||||
r.Post("/:resourceID/builtInRoles/:builtInRole", resourceResolver, licenseMW, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setBuiltinRolePermission))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -435,13 +445,3 @@ func permissionSetResponse(cmd setPermissionCommand) response.Response {
|
||||
}
|
||||
return response.Success(message)
|
||||
}
|
||||
|
||||
func MiddlewareReceiverUIDResolver(paramName string) web.Handler {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
gotParams := web.Params(c.Req)
|
||||
if uid, ok := gotParams[paramName]; ok {
|
||||
gotParams[paramName] = alertingac.ScopeReceiversProvider.GetResourceIDFromUID(uid)
|
||||
web.SetURLParams(c.Req, gotParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
type ResourceValidator func(ctx context.Context, orgID int64, resourceID string) error
|
||||
type InheritedScopesSolver func(ctx context.Context, orgID int64, resourceID string) ([]string, error)
|
||||
|
||||
type ResourceTranslator func(ctx context.Context, orgID int64, resourceID string) (string, error)
|
||||
type Options struct {
|
||||
// Resource is the action and scope prefix that is generated
|
||||
Resource string
|
||||
@ -18,6 +18,9 @@ type Options struct {
|
||||
ResourceAttribute string
|
||||
// OnlyManaged will tell the service to return all permissions if set to false and only managed permissions if set to true
|
||||
OnlyManaged bool
|
||||
// ResourceTranslator is a translator function that will be called before each action, it can be used to translate a resource id to a different format.
|
||||
// If set to nil the translator will be skipped
|
||||
ResourceTranslator ResourceTranslator
|
||||
// ResourceValidator is a validator function that will be called before each assignment.
|
||||
// If set to nil the validator will be skipped
|
||||
ResourceValidator ResourceValidator
|
||||
|
@ -31,30 +31,6 @@ type ServiceAccountsAPI struct {
|
||||
isExternalSAEnabled bool
|
||||
}
|
||||
|
||||
func MiddlewareServiceAccountUIDResolver(saService serviceaccounts.Service, paramName string) web.Handler {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
// Get service account id from request
|
||||
saUID := web.Params(c.Req)[paramName]
|
||||
// if saID is empty or is an integer, we assume it's a service account id and we don't need to resolve it
|
||||
_, err := strconv.ParseInt(saUID, 10, 64)
|
||||
if saUID == "" || err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
serviceAccount, err := saService.RetrieveServiceAccount(c.Req.Context(), &serviceaccounts.GetServiceAccountQuery{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
UID: saUID,
|
||||
})
|
||||
if err == nil {
|
||||
gotParams := web.Params(c.Req)
|
||||
gotParams[paramName] = strconv.FormatInt(serviceAccount.Id, 10)
|
||||
web.SetURLParams(c.Req, gotParams)
|
||||
} else {
|
||||
c.JsonApiErr(http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewServiceAccountsAPI(
|
||||
cfg *setting.Cfg,
|
||||
service serviceaccounts.Service,
|
||||
@ -79,7 +55,7 @@ func NewServiceAccountsAPI(
|
||||
|
||||
func (api *ServiceAccountsAPI) RegisterAPIEndpoints() {
|
||||
auth := accesscontrol.Middleware(api.accesscontrol)
|
||||
saUIDResolver := MiddlewareServiceAccountUIDResolver(api.service, ":serviceAccountId")
|
||||
saUIDResolver := serviceaccounts.MiddlewareServiceAccountUIDResolver(api.service, ":serviceAccountId")
|
||||
api.RouterRegister.Group("/api/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) {
|
||||
serviceAccountsRoute.Get("/search", auth(accesscontrol.EvalPermission(serviceaccounts.ActionRead)), routing.Wrap(api.SearchOrgServiceAccountsWithPaging))
|
||||
serviceAccountsRoute.Post("/", auth(accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.CreateServiceAccount))
|
||||
|
@ -13,13 +13,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
// ServiceAccountRetriever is the service that retrieves service accounts.
|
||||
// At the time of writing, this service is only used by the service accounts permissions service
|
||||
// to avoid cyclic dependency between the ServiceAccountService and the ServiceAccountPermissionsService
|
||||
type ServiceAccountRetriever interface {
|
||||
RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error)
|
||||
}
|
||||
|
||||
// ServiceAccountRetriever is the service that manages service accounts.
|
||||
type Service struct {
|
||||
store *database.ServiceAccountsStoreImpl
|
||||
|
@ -2,8 +2,12 @@ package serviceaccounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/apikey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -13,11 +17,18 @@ Service accounts are used to authenticate API requests. They are not users and
|
||||
do not have a password.
|
||||
*/
|
||||
|
||||
// ServiceAccountRetriever is the service that retrieves service accounts.
|
||||
// At the time of writing, this service is only used by the service accounts permissions service
|
||||
// to avoid cyclic dependency between the ServiceAccountService and the ServiceAccountPermissionsService
|
||||
type ServiceAccountRetriever interface {
|
||||
RetrieveServiceAccount(ctx context.Context, query *GetServiceAccountQuery) (*ServiceAccountProfileDTO, error)
|
||||
}
|
||||
|
||||
//go:generate mockery --name Service --structname MockServiceAccountService --output tests --outpkg tests --filename mocks.go
|
||||
type Service interface {
|
||||
ServiceAccountRetriever
|
||||
CreateServiceAccount(ctx context.Context, orgID int64, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error)
|
||||
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error
|
||||
RetrieveServiceAccount(ctx context.Context, query *GetServiceAccountQuery) (*ServiceAccountProfileDTO, error)
|
||||
RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error)
|
||||
SearchOrgServiceAccounts(ctx context.Context, query *SearchOrgServiceAccountsQuery) (*SearchOrgServiceAccountsResult, error)
|
||||
EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error
|
||||
@ -46,3 +57,35 @@ type ExtSvcAccountsService interface {
|
||||
// RetrieveExtSvcAccount fetches an external service account by ID
|
||||
RetrieveExtSvcAccount(ctx context.Context, orgID, saID int64) (*ExtSvcAccount, error)
|
||||
}
|
||||
|
||||
func UIDToIDHandler(saService ServiceAccountRetriever) func(ctx context.Context, orgID int64, resourceID string) (string, error) {
|
||||
return func(ctx context.Context, orgID int64, resourceID string) (string, error) {
|
||||
// if saID is empty or is an integer, we assume it's a service account id and we don't need to resolve it
|
||||
_, err := strconv.ParseInt(resourceID, 10, 64)
|
||||
if resourceID == "" || err == nil {
|
||||
return resourceID, nil
|
||||
}
|
||||
serviceAccount, err := saService.RetrieveServiceAccount(ctx, &GetServiceAccountQuery{
|
||||
OrgID: orgID,
|
||||
UID: resourceID,
|
||||
})
|
||||
return strconv.FormatInt(serviceAccount.Id, 10), err
|
||||
}
|
||||
}
|
||||
|
||||
func MiddlewareServiceAccountUIDResolver(saService Service, paramName string) web.Handler {
|
||||
handler := UIDToIDHandler(saService)
|
||||
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
// Get sa id from request, fetch service account and replace saUID with saID
|
||||
saUID := web.Params(c.Req)[paramName]
|
||||
id, err := handler(c.Req.Context(), c.SignedInUser.GetOrgID(), saUID)
|
||||
if err == nil {
|
||||
gotParams := web.Params(c.Req)
|
||||
gotParams[paramName] = id
|
||||
web.SetURLParams(c.Req, gotParams)
|
||||
} else {
|
||||
c.JsonApiErr(http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package team
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
@ -23,23 +24,31 @@ type Service interface {
|
||||
RegisterDelete(query string)
|
||||
}
|
||||
|
||||
func UIDToIDHandler(teamService Service) func(ctx context.Context, orgID int64, resourceID string) (string, error) {
|
||||
return func(ctx context.Context, orgID int64, resourceID string) (string, error) {
|
||||
// if teamID is empty or is an integer, we assume it's a team id and we don't need to resolve it
|
||||
_, err := strconv.ParseInt(resourceID, 10, 64)
|
||||
if resourceID == "" || err == nil {
|
||||
return resourceID, nil
|
||||
}
|
||||
team, err := teamService.GetTeamByID(ctx, &GetTeamByIDQuery{UID: resourceID, OrgID: orgID})
|
||||
return strconv.FormatInt(team.ID, 10), err
|
||||
}
|
||||
}
|
||||
|
||||
func MiddlewareTeamUIDResolver(teamService Service, paramName string) web.Handler {
|
||||
handler := UIDToIDHandler(teamService)
|
||||
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
// Get team id from request, fetch team and replace teamId with team id
|
||||
teamID := web.Params(c.Req)[paramName]
|
||||
// if teamID is empty or is an integer, we assume it's a team id and we don't need to resolve it
|
||||
_, err := strconv.ParseInt(teamID, 10, 64)
|
||||
if teamID == "" || err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
team, err := teamService.GetTeamByID(c.Req.Context(), &GetTeamByIDQuery{UID: teamID, OrgID: c.OrgID})
|
||||
id, err := handler(c.Req.Context(), c.OrgID, teamID)
|
||||
if err == nil {
|
||||
gotParams := web.Params(c.Req)
|
||||
gotParams[paramName] = strconv.FormatInt(team.ID, 10)
|
||||
gotParams[paramName] = id
|
||||
web.SetURLParams(c.Req, gotParams)
|
||||
} else {
|
||||
c.JsonApiErr(404, "Not found", nil)
|
||||
c.JsonApiErr(http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export const ServiceAccountPermissions = (props: ServiceAccountPermissionsProps)
|
||||
addPermissionTitle="Add permission"
|
||||
buttonLabel="Add permission"
|
||||
resource="serviceaccounts"
|
||||
resourceId={props.serviceAccount.id}
|
||||
resourceId={props.serviceAccount.uid}
|
||||
canSetPermissions={canSetPermissions}
|
||||
/>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user