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:
Jo 2024-10-29 10:21:39 +01:00 committed by GitHub
parent fb51a5e21f
commit 2b867d9850
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 102 additions and 73 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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}
/>
);