190 lines
6.4 KiB
Go
190 lines
6.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
|
)
|
|
|
|
type OperationGroup string
|
|
|
|
const (
|
|
OperationGroupFolder OperationGroup = "folder"
|
|
OperationGroupPermission OperationGroup = "permission"
|
|
OperationGroupUserOrgRole OperationGroup = "user_org_role"
|
|
OperationGroupRoleBinding OperationGroup = "role_binding"
|
|
OperationGroupTeamBinding OperationGroup = "team_binding"
|
|
OperationGroupRole OperationGroup = "role"
|
|
)
|
|
|
|
func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) {
|
|
ctx, span := s.tracer.Start(ctx, "server.Mutate")
|
|
defer span.End()
|
|
|
|
defer func(t time.Time) {
|
|
s.metrics.requestDurationSeconds.WithLabelValues("server.Mutate", req.GetNamespace()).Observe(time.Since(t).Seconds())
|
|
}(time.Now())
|
|
|
|
res, err := s.mutate(ctx, req)
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
if _, ok := status.FromError(err); ok {
|
|
return nil, err
|
|
}
|
|
s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace())
|
|
return nil, errors.New("failed to perform mutate request")
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) {
|
|
if err := authorizeWrite(ctx, req.GetNamespace(), s.cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
storeInf, err := s.getStoreInfo(ctx, req.Namespace)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get openfga store: %w", err)
|
|
}
|
|
|
|
groupedOperations, err := groupByOperation(req.GetOperations())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to group operations: %w", err)
|
|
}
|
|
|
|
for operationGroup, operations := range groupedOperations {
|
|
switch operationGroup {
|
|
case OperationGroupFolder:
|
|
if err := s.mutateFolders(ctx, storeInf, operations); err != nil {
|
|
return nil, fmt.Errorf("failed to mutate folder: %w", err)
|
|
}
|
|
case OperationGroupPermission:
|
|
if err := s.mutateResourcePermissions(ctx, storeInf, operations); err != nil {
|
|
return nil, fmt.Errorf("failed to mutate resource permissions: %w", err)
|
|
}
|
|
case OperationGroupUserOrgRole:
|
|
if err := s.mutateOrgRoles(ctx, storeInf, operations); err != nil {
|
|
return nil, fmt.Errorf("failed to mutate org roles: %w", err)
|
|
}
|
|
case OperationGroupRoleBinding:
|
|
if err := s.mutateRoleBindings(ctx, storeInf, operations); err != nil {
|
|
return nil, fmt.Errorf("failed to mutate role bindings: %w", err)
|
|
}
|
|
case OperationGroupTeamBinding:
|
|
if err := s.mutateTeamBindings(ctx, storeInf, operations); err != nil {
|
|
return nil, fmt.Errorf("failed to mutate team bindings: %w", err)
|
|
}
|
|
case OperationGroupRole:
|
|
if err := s.mutateRoles(ctx, storeInf, operations); err != nil {
|
|
return nil, fmt.Errorf("failed to mutate roles: %w", err)
|
|
}
|
|
default:
|
|
s.logger.Warn("unsupported operation group", "operationGroup", operationGroup)
|
|
}
|
|
}
|
|
|
|
return &authzextv1.MutateResponse{}, nil
|
|
}
|
|
|
|
func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, error) {
|
|
switch operation.Operation.(type) {
|
|
case *authzextv1.MutateOperation_SetFolderParent, *authzextv1.MutateOperation_DeleteFolder:
|
|
return OperationGroupFolder, nil
|
|
case *authzextv1.MutateOperation_CreatePermission, *authzextv1.MutateOperation_DeletePermission:
|
|
return OperationGroupPermission, nil
|
|
case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole, *authzextv1.MutateOperation_AddUserOrgRole:
|
|
return OperationGroupUserOrgRole, nil
|
|
case *authzextv1.MutateOperation_CreateRoleBinding, *authzextv1.MutateOperation_DeleteRoleBinding:
|
|
return OperationGroupRoleBinding, nil
|
|
case *authzextv1.MutateOperation_CreateTeamBinding, *authzextv1.MutateOperation_DeleteTeamBinding:
|
|
return OperationGroupTeamBinding, nil
|
|
case *authzextv1.MutateOperation_CreateRole, *authzextv1.MutateOperation_DeleteRole:
|
|
return OperationGroupRole, nil
|
|
}
|
|
return OperationGroup(""), errors.New("unsupported mutate operation type")
|
|
}
|
|
|
|
func groupByOperation(operations []*authzextv1.MutateOperation) (map[OperationGroup][]*authzextv1.MutateOperation, error) {
|
|
grouped := make(map[OperationGroup][]*authzextv1.MutateOperation)
|
|
for _, operation := range operations {
|
|
operationGroup, err := getOperationGroup(operation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
grouped[operationGroup] = append(grouped[operationGroup], operation)
|
|
}
|
|
|
|
return grouped, nil
|
|
}
|
|
|
|
func deduplicateTupleKeys(writeTuples []*openfgav1.TupleKey, deleteTuples []*openfgav1.TupleKeyWithoutCondition) ([]*openfgav1.TupleKey, []*openfgav1.TupleKeyWithoutCondition) {
|
|
deduplicatedWriteTuples := make([]*openfgav1.TupleKey, 0)
|
|
deduplicatedDeleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0)
|
|
|
|
writeTupleMap := make(map[string]bool)
|
|
|
|
for _, writeTuple := range writeTuples {
|
|
id := getTupleKeyID(writeTuple)
|
|
if !writeTupleMap[id] {
|
|
writeTupleMap[id] = true
|
|
deduplicatedWriteTuples = append(deduplicatedWriteTuples, writeTuple)
|
|
}
|
|
}
|
|
|
|
// Prioritize writes over deletes. Deletes do not have a condition, so we don't know if write tuple is different from delete one.
|
|
for _, deleteTuple := range deleteTuples {
|
|
id := getTupleKeyID(deleteTuple)
|
|
if !writeTupleMap[id] {
|
|
writeTupleMap[id] = true
|
|
deduplicatedDeleteTuples = append(deduplicatedDeleteTuples, deleteTuple)
|
|
}
|
|
}
|
|
|
|
return deduplicatedWriteTuples, deduplicatedDeleteTuples
|
|
}
|
|
|
|
func (s *Server) writeTuples(ctx context.Context, store *storeInfo, writeTuples []*openfgav1.TupleKey, deleteTuples []*openfgav1.TupleKeyWithoutCondition) error {
|
|
writeReq := &openfgav1.WriteRequest{
|
|
StoreId: store.ID,
|
|
AuthorizationModelId: store.ModelID,
|
|
}
|
|
|
|
writeTuples, deleteTuples = deduplicateTupleKeys(writeTuples, deleteTuples)
|
|
|
|
if len(writeTuples) > 0 {
|
|
writeReq.Writes = &openfgav1.WriteRequestWrites{
|
|
TupleKeys: writeTuples,
|
|
OnDuplicate: "ignore",
|
|
}
|
|
}
|
|
|
|
if len(deleteTuples) > 0 {
|
|
writeReq.Deletes = &openfgav1.WriteRequestDeletes{
|
|
TupleKeys: deleteTuples,
|
|
OnMissing: "ignore",
|
|
}
|
|
}
|
|
|
|
_, err := s.openfga.Write(ctx, writeReq)
|
|
return err
|
|
}
|
|
|
|
type TupleKey interface {
|
|
GetUser() string
|
|
GetRelation() string
|
|
GetObject() string
|
|
}
|
|
|
|
func getTupleKeyID(t TupleKey) string {
|
|
return fmt.Sprintf("%s:%s:%s", t.GetUser(), t.GetRelation(), t.GetObject())
|
|
}
|