From 2b867d985073a852ffa1dd5d03515081c3ab3405 Mon Sep 17 00:00:00 2001 From: Jo Date: Tue, 29 Oct 2024 10:21:39 +0100 Subject: [PATCH] AccessControl: Resource permission translator (#95423) * Standardize Resource Translator in options * Add resource UID translator for resource permissions * fix comments * fix comments --- pkg/server/wire.go | 2 +- .../ossaccesscontrol/receivers.go | 3 ++ .../ossaccesscontrol/service_account.go | 5 +- .../accesscontrol/ossaccesscontrol/team.go | 7 +-- .../accesscontrol/resourcepermissions/api.go | 46 +++++++++---------- .../resourcepermissions/options.go | 5 +- pkg/services/serviceaccounts/api/api.go | 26 +---------- .../serviceaccounts/retriever/retriever.go | 7 --- .../serviceaccounts/serviceaccounts.go | 45 +++++++++++++++++- pkg/services/team/team.go | 27 +++++++---- .../ServiceAccountPermissions.tsx | 2 +- 11 files changed, 102 insertions(+), 73 deletions(-) diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 1d31c7b1d35..d6c651b162c 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -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, diff --git a/pkg/services/accesscontrol/ossaccesscontrol/receivers.go b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go index 8724b0ee29b..2f6ec0930a8 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/receivers.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go @@ -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, diff --git a/pkg/services/accesscontrol/ossaccesscontrol/service_account.go b/pkg/services/accesscontrol/ossaccesscontrol/service_account.go index 76a128c8193..068c8c91544 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/service_account.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/service_account.go @@ -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() diff --git a/pkg/services/accesscontrol/ossaccesscontrol/team.go b/pkg/services/accesscontrol/ossaccesscontrol/team.go index 619465e5569..a2635987fb3 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/team.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/team.go @@ -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() diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 006139b6539..f2ad06060c0 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -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) - } - } -} diff --git a/pkg/services/accesscontrol/resourcepermissions/options.go b/pkg/services/accesscontrol/resourcepermissions/options.go index cea4888a911..01d40b5834e 100644 --- a/pkg/services/accesscontrol/resourcepermissions/options.go +++ b/pkg/services/accesscontrol/resourcepermissions/options.go @@ -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 diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index cfd850aa645..fbe018a4834 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -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)) diff --git a/pkg/services/serviceaccounts/retriever/retriever.go b/pkg/services/serviceaccounts/retriever/retriever.go index a3f8d57e755..de269238af7 100644 --- a/pkg/services/serviceaccounts/retriever/retriever.go +++ b/pkg/services/serviceaccounts/retriever/retriever.go @@ -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 diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index daee2197a75..d63b5c92ea2 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -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) + } + } +} diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 3df15bcddc1..74620af9f32 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -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) } } } diff --git a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx index 95be8c782eb..46455373073 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx @@ -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} /> );