Files
grafana/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go
2025-12-05 13:55:56 +01:00

157 lines
4.9 KiB
Go

package server
import (
"context"
"errors"
"fmt"
"strings"
"google.golang.org/protobuf/types/known/structpb"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
var (
errEmptyName = errors.New("name cannot be empty")
errInvalidBasicRole = errors.New("invalid basic role")
errUnknownKind = errors.New("unknown permission kind")
)
func (s *Server) mutateResourcePermissions(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error {
ctx, span := s.tracer.Start(ctx, "server.mutateResourcePermissions")
defer span.End()
writeTuples := make([]*openfgav1.TupleKey, 0)
deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0)
for _, operation := range operations {
switch op := operation.Operation.(type) {
case *authzextv1.MutateOperation_CreatePermission:
tuple, err := s.getPermissionWriteTuple(ctx, op.CreatePermission)
if err != nil {
return err
}
writeTuples = append(writeTuples, tuple)
case *authzextv1.MutateOperation_DeletePermission:
tuple, err := s.getPermissionDeleteTuple(ctx, op.DeletePermission)
if err != nil {
return err
}
deleteTuples = append(deleteTuples, tuple)
default:
s.logger.Debug("unsupported mutate operation", "operation", op)
}
}
err := s.writeTuples(ctx, store, writeTuples, deleteTuples)
if err != nil {
s.logger.Error("failed to write resource permission tuples", "error", err)
return err
}
return nil
}
func (s *Server) getPermissionWriteTuple(ctx context.Context, req *authzextv1.CreatePermissionOperation) (*openfgav1.TupleKey, error) {
resource := req.GetResource()
permission := req.GetPermission()
object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName())
tuple, err := NewResourceTuple(object, resource, permission)
if err != nil {
return nil, err
}
return tuple, nil
}
func (s *Server) getPermissionDeleteTuple(ctx context.Context, req *authzextv1.DeletePermissionOperation) (*openfgav1.TupleKeyWithoutCondition, error) {
resource := req.GetResource()
permission := req.GetPermission()
object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName())
tuple, err := NewResourceTuple(object, resource, permission)
if err != nil {
return nil, err
}
return &openfgav1.TupleKeyWithoutCondition{
User: tuple.GetUser(),
Relation: tuple.GetRelation(),
Object: tuple.GetObject(),
}, nil
}
func toZanzanaType(apiGroup string) string {
if apiGroup == "folder.grafana.app" {
return zanzana.TypeFolder
}
return zanzana.TypeResource
}
func NewResourceTuple(object string, resource *authzextv1.Resource, perm *authzextv1.Permission) (*openfgav1.TupleKey, error) {
// Typ is "folder" or "resource"
typ := toZanzanaType(resource.Group)
// subject
subject, err := toZanzanaSubject(perm.GetKind(), perm.GetName())
if err != nil {
return nil, err
}
key := &openfgav1.TupleKey{
// e.g. "user:{uid}", "serviceaccount:{uid}", "team:{uid}", "basicrole:{viewer|editor|admin}"
User: subject,
// "view", "edit", "admin"
Relation: strings.ToLower(perm.Verb),
// e.g. "folder:{name}" or "resource:{apiGroup}/{resource}/{name}"
Object: object,
}
// For resources we add a condition to filter by apiGroup/resource
// e.g "group_filter": {"group_resource": "dashboards.grafana.app/dashboards"}
if typ == zanzana.TypeResource {
key.Condition = &openfgav1.RelationshipCondition{
Name: "group_filter",
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"group_resource": structpb.NewStringValue(
resource.GetGroup() + "/" + resource.GetResource(),
),
},
},
}
}
return key, nil
}
func toZanzanaSubject(kind string, name string) (string, error) {
if name == "" {
return "", errEmptyName
}
iamKind := iamv0.ResourcePermissionSpecPermissionKind(kind)
switch iamKind {
case iamv0.ResourcePermissionSpecPermissionKindUser:
return zanzana.NewTupleEntry(zanzana.TypeUser, name, ""), nil
case iamv0.ResourcePermissionSpecPermissionKindServiceAccount:
return zanzana.NewTupleEntry(zanzana.TypeServiceAccount, name, ""), nil
case iamv0.ResourcePermissionSpecPermissionKindTeam:
return zanzana.NewTupleEntry(zanzana.TypeTeam, name, ""), nil
case iamv0.ResourcePermissionSpecPermissionKindBasicRole:
basicRole := zanzana.TranslateBasicRole(name)
if basicRole == "" {
return "", fmt.Errorf("%w: %s", errInvalidBasicRole, name)
}
// e.g role:basic_viewer#assignee
return zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, zanzana.RelationAssignee), nil
}
// should not happen since we are after create
// validation webhook should have caught invalid kinds
return "", errUnknownKind
}