mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 22:24:31 +08:00
511 lines
16 KiB
Go
511 lines
16 KiB
Go
package resourcepermissions
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/api/routing"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/team"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
)
|
|
|
|
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/resourcepermissions")
|
|
|
|
type api struct {
|
|
cfg *setting.Cfg
|
|
ac accesscontrol.AccessControl
|
|
router routing.RouteRegister
|
|
service *Service
|
|
permissions []string
|
|
}
|
|
|
|
func newApi(cfg *setting.Cfg, ac accesscontrol.AccessControl, router routing.RouteRegister, manager *Service) *api {
|
|
permissions := make([]string, 0, len(manager.permissions))
|
|
// reverse the permissions order for display
|
|
for i := len(manager.permissions) - 1; i >= 0; i-- {
|
|
permissions = append(permissions, manager.permissions[i])
|
|
}
|
|
return &api{cfg, ac, router, manager, permissions}
|
|
}
|
|
|
|
func (a *api) registerEndpoints() {
|
|
auth := accesscontrol.Middleware(a.ac)
|
|
licenseMW := a.service.options.LicenseMW
|
|
if licenseMW == nil {
|
|
licenseMW = nopMiddleware
|
|
}
|
|
|
|
userUIDResolver := middlewareUserUIDResolver(a.service.userService, ":userID")
|
|
teamUIDResolver := team.MiddlewareTeamUIDResolver(a.service.teamService, ":teamID")
|
|
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", 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, resourceResolver, userUIDResolver, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setUserPermission))
|
|
}
|
|
if a.service.options.Assignments.Teams {
|
|
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", resourceResolver, licenseMW, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setBuiltinRolePermission))
|
|
}
|
|
})
|
|
}
|
|
|
|
type Assignments struct {
|
|
Users bool `json:"users"`
|
|
ServiceAccounts bool `json:"serviceAccounts"`
|
|
Teams bool `json:"teams"`
|
|
BuiltInRoles bool `json:"builtInRoles"`
|
|
}
|
|
|
|
// swagger:parameters getResourceDescription
|
|
type GetResourceDescriptionParams struct {
|
|
// in:path
|
|
// required:true
|
|
Resource string `json:"resource"`
|
|
}
|
|
|
|
// swagger:response resourcePermissionsDescription
|
|
type DescriptionResponse struct {
|
|
// in:body
|
|
// required:true
|
|
Body Description `json:"body"`
|
|
}
|
|
|
|
type Description struct {
|
|
Assignments Assignments `json:"assignments"`
|
|
Permissions []string `json:"permissions"`
|
|
}
|
|
|
|
// swagger:route GET /access-control/{resource}/description access_control getResourceDescription
|
|
//
|
|
// Get a description of a resource's access control properties.
|
|
//
|
|
// Responses:
|
|
// 200: resourcePermissionsDescription
|
|
// 403: forbiddenError
|
|
// 500: internalServerError
|
|
func (a *api) getDescription(c *contextmodel.ReqContext) response.Response {
|
|
return response.JSON(http.StatusOK, &Description{
|
|
Permissions: a.permissions,
|
|
Assignments: a.service.options.Assignments,
|
|
})
|
|
}
|
|
|
|
type resourcePermissionDTO struct {
|
|
ID int64 `json:"id"`
|
|
RoleName string `json:"roleName"`
|
|
IsManaged bool `json:"isManaged"`
|
|
IsInherited bool `json:"isInherited"`
|
|
IsServiceAccount bool `json:"isServiceAccount"`
|
|
UserID int64 `json:"userId,omitempty"`
|
|
UserUID string `json:"userUid,omitempty"`
|
|
UserLogin string `json:"userLogin,omitempty"`
|
|
UserAvatarUrl string `json:"userAvatarUrl,omitempty"`
|
|
Team string `json:"team,omitempty"`
|
|
TeamID int64 `json:"teamId,omitempty"`
|
|
TeamUID string `json:"teamUid,omitempty"`
|
|
TeamAvatarUrl string `json:"teamAvatarUrl,omitempty"`
|
|
BuiltInRole string `json:"builtInRole,omitempty"`
|
|
Actions []string `json:"actions"`
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
// swagger:parameters getResourcePermissions
|
|
type GetResourcePermissionsParams struct {
|
|
// in:path
|
|
// required:true
|
|
Resource string `json:"resource"`
|
|
|
|
// in:path
|
|
// required:true
|
|
ResourceID string `json:"resourceID"`
|
|
}
|
|
|
|
// swagger:response getResourcePermissionsResponse
|
|
type getResourcePermissionsResponse []resourcePermissionDTO
|
|
|
|
// swagger:route GET /access-control/{resource}/{resourceID} access_control getResourcePermissions
|
|
//
|
|
// Get permissions for a resource.
|
|
//
|
|
// Responses:
|
|
// 200: getResourcePermissionsResponse
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response {
|
|
ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.resourcepermissions.getPermissions")
|
|
defer span.End()
|
|
c.Req = c.Req.WithContext(ctx)
|
|
|
|
resourceID := web.Params(c.Req)[":resourceID"]
|
|
|
|
permissions, err := a.service.GetPermissions(c.Req.Context(), c.SignedInUser, resourceID)
|
|
if err != nil {
|
|
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to get permissions", err)
|
|
}
|
|
|
|
if a.service.options.Assignments.BuiltInRoles && !a.service.license.FeatureEnabled("accesscontrol.enforcement") {
|
|
permissions = append(permissions, accesscontrol.ResourcePermission{
|
|
Actions: a.service.actions,
|
|
Scope: "*",
|
|
BuiltInRole: string(org.RoleAdmin),
|
|
})
|
|
}
|
|
|
|
dto := make(getResourcePermissionsResponse, 0, len(permissions))
|
|
for _, p := range permissions {
|
|
if permission := a.service.MapActions(p); permission != "" {
|
|
teamAvatarUrl := ""
|
|
if p.TeamID != 0 {
|
|
teamAvatarUrl = dtos.GetGravatarUrlWithDefault(a.cfg, p.TeamEmail, p.Team)
|
|
}
|
|
|
|
dto = append(dto, resourcePermissionDTO{
|
|
ID: p.ID,
|
|
RoleName: p.RoleName,
|
|
UserID: p.UserID,
|
|
UserUID: p.UserUID,
|
|
UserLogin: p.UserLogin,
|
|
UserAvatarUrl: dtos.GetGravatarUrl(a.cfg, p.UserEmail),
|
|
Team: p.Team,
|
|
TeamID: p.TeamID,
|
|
TeamUID: p.TeamUID,
|
|
TeamAvatarUrl: teamAvatarUrl,
|
|
BuiltInRole: p.BuiltInRole,
|
|
Actions: p.Actions,
|
|
Permission: permission,
|
|
IsManaged: p.IsManaged,
|
|
IsInherited: p.IsInherited,
|
|
IsServiceAccount: p.IsServiceAccount,
|
|
})
|
|
}
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, dto)
|
|
}
|
|
|
|
type setPermissionCommand struct {
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
type setPermissionsCommand struct {
|
|
Permissions []accesscontrol.SetResourcePermissionCommand `json:"permissions"`
|
|
}
|
|
|
|
// swagger:parameters setResourcePermissionsForUser
|
|
type SetResourcePermissionsForUserParams struct {
|
|
// in:path
|
|
// required:true
|
|
Resource string `json:"resource"`
|
|
|
|
// in:path
|
|
// required:true
|
|
ResourceID string `json:"resourceID"`
|
|
|
|
// in:path
|
|
// required:true
|
|
UserID int64 `json:"userID"`
|
|
|
|
// in:body
|
|
// required:true
|
|
Body setPermissionCommand
|
|
}
|
|
|
|
// swagger:route POST /access-control/{resource}/{resourceID}/users/{userID} access_control setResourcePermissionsForUser
|
|
//
|
|
// Set resource permissions for a user.
|
|
//
|
|
// Assigns permissions for a resource by a given type (`:resource`) and `:resourceID` to a user or a service account.
|
|
// Allowed resources are `datasources`, `teams`, `dashboards`, `folders`, and `serviceaccounts`.
|
|
// Refer to the `/access-control/{resource}/description` endpoint for allowed Permissions.
|
|
//
|
|
// Responses:
|
|
// 200: okResponse
|
|
// 400: badRequestError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (a *api) setUserPermission(c *contextmodel.ReqContext) response.Response {
|
|
ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.resourcepermissions.setUserPermission")
|
|
defer span.End()
|
|
c.Req = c.Req.WithContext(ctx)
|
|
|
|
userID, err := strconv.ParseInt(web.Params(c.Req)[":userID"], 10, 64)
|
|
if err != nil {
|
|
return response.Err(ErrInvalidParam.Build(ErrInvalidParamData("userID", err)))
|
|
}
|
|
resourceID := web.Params(c.Req)[":resourceID"]
|
|
|
|
resp := a.validateTeamResource(c, resourceID)
|
|
if resp != nil {
|
|
return resp
|
|
}
|
|
|
|
var cmd setPermissionCommand
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
|
|
_, err = a.service.SetUserPermission(c.Req.Context(), c.GetOrgID(), accesscontrol.User{ID: userID}, resourceID, cmd.Permission)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
|
|
return permissionSetResponse(cmd)
|
|
}
|
|
|
|
// swagger:parameters setResourcePermissionsForTeam
|
|
type SetResourcePermissionsForTeamParams struct {
|
|
// in:path
|
|
// required:true
|
|
Resource string `json:"resource"`
|
|
|
|
// in:path
|
|
// required:true
|
|
ResourceID string `json:"resourceID"`
|
|
|
|
// in:path
|
|
// required:true
|
|
TeamID int64 `json:"teamID"`
|
|
|
|
// in:body
|
|
// required:true
|
|
Body setPermissionCommand
|
|
}
|
|
|
|
// swagger:route POST /access-control/{resource}/{resourceID}/teams/{teamID} access_control setResourcePermissionsForTeam
|
|
//
|
|
// Set resource permissions for a team.
|
|
//
|
|
// Assigns permissions for a resource by a given type (`:resource`) and `:resourceID` to a team.
|
|
// Allowed resources are `datasources`, `teams`, `dashboards`, `folders`, and `serviceaccounts`.
|
|
// Refer to the `/access-control/{resource}/description` endpoint for allowed Permissions.
|
|
//
|
|
// Responses:
|
|
// 200: okResponse
|
|
// 400: badRequestError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (a *api) setTeamPermission(c *contextmodel.ReqContext) response.Response {
|
|
ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.resourcepermissions.setTeamPermission")
|
|
defer span.End()
|
|
c.Req = c.Req.WithContext(ctx)
|
|
|
|
teamID, err := strconv.ParseInt(web.Params(c.Req)[":teamID"], 10, 64)
|
|
if err != nil {
|
|
return response.Err(ErrInvalidParam.Build(ErrInvalidParamData("teamID", err)))
|
|
}
|
|
resourceID := web.Params(c.Req)[":resourceID"]
|
|
|
|
var cmd setPermissionCommand
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
|
|
_, err = a.service.SetTeamPermission(c.Req.Context(), c.GetOrgID(), teamID, resourceID, cmd.Permission)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
|
|
return permissionSetResponse(cmd)
|
|
}
|
|
|
|
// swagger:parameters setResourcePermissionsForBuiltInRole
|
|
type SetResourcePermissionsForBuiltInRoleParams struct {
|
|
// in:path
|
|
// required:true
|
|
Resource string `json:"resource"`
|
|
|
|
// in:path
|
|
// required:true
|
|
ResourceID string `json:"resourceID"`
|
|
|
|
// in:path
|
|
// required:true
|
|
BuiltInRole string `json:"builtInRole"`
|
|
|
|
// in:body
|
|
// required:true
|
|
Body setPermissionCommand
|
|
}
|
|
|
|
// swagger:route POST /access-control/{resource}/{resourceID}/builtInRoles/{builtInRole} access_control setResourcePermissionsForBuiltInRole
|
|
//
|
|
// Set resource permissions for a built-in role.
|
|
//
|
|
// Assigns permissions for a resource by a given type (`:resource`) and `:resourceID` to a built-in role.
|
|
// Allowed resources are `datasources`, `teams`, `dashboards`, `folders`, and `serviceaccounts`.
|
|
// Refer to the `/access-control/{resource}/description` endpoint for allowed Permissions.
|
|
//
|
|
// Responses:
|
|
// 200: okResponse
|
|
// 400: badRequestError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (a *api) setBuiltinRolePermission(c *contextmodel.ReqContext) response.Response {
|
|
ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.resourcepermissions.setBuiltinRolePermission")
|
|
defer span.End()
|
|
c.Req = c.Req.WithContext(ctx)
|
|
|
|
builtInRole := web.Params(c.Req)[":builtInRole"]
|
|
resourceID := web.Params(c.Req)[":resourceID"]
|
|
|
|
cmd := setPermissionCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
|
|
_, err := a.service.SetBuiltInRolePermission(c.Req.Context(), c.GetOrgID(), builtInRole, resourceID, cmd.Permission)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
|
|
return permissionSetResponse(cmd)
|
|
}
|
|
|
|
// swagger:parameters setResourcePermissions
|
|
type SetResourcePermissionsParams struct {
|
|
// in:path
|
|
// required:true
|
|
Resource string `json:"resource"`
|
|
|
|
// in:path
|
|
// required:true
|
|
ResourceID string `json:"resourceID"`
|
|
|
|
// in:body
|
|
// required:true
|
|
Body setPermissionsCommand
|
|
}
|
|
|
|
// swagger:route POST /access-control/{resource}/{resourceID} access_control setResourcePermissions
|
|
//
|
|
// Set resource permissions.
|
|
//
|
|
// Assigns permissions for a resource by a given type (`:resource`) and `:resourceID` to one or many
|
|
// assignment types. Allowed resources are `datasources`, `teams`, `dashboards`, `folders`, and `serviceaccounts`.
|
|
// Refer to the `/access-control/{resource}/description` endpoint for allowed Permissions.
|
|
//
|
|
// Responses:
|
|
// 200: okResponse
|
|
// 400: badRequestError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (a *api) setPermissions(c *contextmodel.ReqContext) response.Response {
|
|
ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.resourcepermissions.setPermissions")
|
|
defer span.End()
|
|
|
|
resourceID := web.Params(c.Req)[":resourceID"]
|
|
|
|
cmd := setPermissionsCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "Bad request data: "+err.Error(), err)
|
|
}
|
|
|
|
_, err := a.service.SetPermissions(ctx, c.GetOrgID(), resourceID, cmd.Permissions...)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
|
|
return response.Success("Permissions updated")
|
|
}
|
|
|
|
func (a *api) validateTeamResource(c *contextmodel.ReqContext, resourceID string) response.Response {
|
|
if a.service.options.Resource != "teams" {
|
|
return nil
|
|
}
|
|
|
|
teamID, err := strconv.ParseInt(resourceID, 10, 64)
|
|
if err != nil {
|
|
return response.Error(http.StatusBadRequest, "Invalid ResourceID", err)
|
|
}
|
|
|
|
existingTeam, err := a.service.teamService.GetTeamByID(c.Req.Context(), &team.GetTeamByIDQuery{
|
|
OrgID: c.GetOrgID(),
|
|
ID: teamID,
|
|
SignedInUser: c.SignedInUser,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, team.ErrTeamNotFound) {
|
|
return response.Error(http.StatusNotFound, "Team not found", err)
|
|
}
|
|
|
|
return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
|
|
}
|
|
|
|
if existingTeam.IsProvisioned {
|
|
return response.Error(http.StatusBadRequest, "Team permissions cannot be updated for provisioned teams", nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func permissionSetResponse(cmd setPermissionCommand) response.Response {
|
|
message := "Permission updated"
|
|
if cmd.Permission == "" {
|
|
message = "Permission removed"
|
|
}
|
|
return response.Success(message)
|
|
}
|
|
|
|
// middlewareUserUIDResolver resolves the user UID to ID and sets the ID in the URL params.
|
|
func middlewareUserUIDResolver(userService user.Service, paramName string) web.Handler {
|
|
handler := user.UIDToIDHandler(userService)
|
|
|
|
return func(c *contextmodel.ReqContext) {
|
|
userID := web.Params(c.Req)[paramName]
|
|
id, err := handler(c.Req.Context(), userID)
|
|
if err == nil {
|
|
gotParams := web.Params(c.Req)
|
|
gotParams[paramName] = id
|
|
web.SetURLParams(c.Req, gotParams)
|
|
} else {
|
|
if errors.Is(err, user.ErrUserNotFound) {
|
|
c.JsonApiErr(http.StatusNotFound, "User not found", nil)
|
|
} else {
|
|
c.JsonApiErr(http.StatusInternalServerError, "Failed to resolve user", err)
|
|
}
|
|
}
|
|
}
|
|
}
|