package server import ( "context" "fmt" "strings" openfgav1 "github.com/openfga/api/proto/openfga/v1" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) func (s *Server) mutateFolders(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { ctx, span := s.tracer.Start(ctx, "server.mutateFolder") 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_SetFolderParent: tuple, err := s.getFolderWriteTuple(ctx, store, op.SetFolderParent) if err != nil { return err } if tuple != nil { writeTuples = append(writeTuples, tuple) } // Delete existing parent tuples if op.SetFolderParent.GetDeleteExisting() { tuples, err := s.getFolderDeleteTuples(ctx, store, op.SetFolderParent.GetFolder(), op.SetFolderParent.GetParent(), true) if err != nil { return err } deleteTuples = append(deleteTuples, tuples...) } case *authzextv1.MutateOperation_DeleteFolder: tuples, err := s.getFolderDeleteTuples(ctx, store, op.DeleteFolder.GetFolder(), op.DeleteFolder.GetParent(), op.DeleteFolder.GetDeleteExisting()) if err != nil { return err } deleteTuples = append(deleteTuples, tuples...) default: s.logger.Debug("unsupported mutate operation", "operation", op) } } if len(writeTuples) == 0 && len(deleteTuples) == 0 { return nil } err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write folder tuples", "error", err) return err } return nil } func (s *Server) getFolderWriteTuple(ctx context.Context, store *storeInfo, req *authzextv1.SetFolderParentOperation) (*openfgav1.TupleKey, error) { // Folder is at the root level if req.GetParent() == "" { return nil, nil } if strings.ContainsAny(req.GetFolder(), "#:") { return nil, fmt.Errorf("folder UID contains invalid characters: %s", req.GetFolder()) } tuple := zanzana.NewFolderParentTuple(req.GetFolder(), req.GetParent()) return tuple, nil } func (s *Server) getFolderDeleteTuples(ctx context.Context, store *storeInfo, folderUID string, parentUID string, deleteExisting bool) ([]*openfgav1.TupleKeyWithoutCondition, error) { tupleKeysToDelete := make([]*openfgav1.TupleKeyWithoutCondition, 0) if folderUID != "" && parentUID != "" && !deleteExisting { tuple := zanzana.NewFolderParentTuple(folderUID, parentUID) tupleKeysToDelete = append(tupleKeysToDelete, &openfgav1.TupleKeyWithoutCondition{ User: tuple.GetUser(), Relation: tuple.GetRelation(), Object: tuple.GetObject(), }) } if deleteExisting { parentTuples, err := s.listFolderParents(ctx, store, folderUID) if err != nil { return nil, fmt.Errorf("failed to list folder parents: %w", err) } for _, tuple := range parentTuples { tupleKeysToDelete = append(tupleKeysToDelete, &openfgav1.TupleKeyWithoutCondition{ User: tuple.Key.User, Relation: tuple.Key.Relation, Object: tuple.Key.Object, }) } } return tupleKeysToDelete, nil } func (s *Server) listFolderParents(ctx context.Context, store *storeInfo, folderUID string) ([]*openfgav1.Tuple, error) { ctx, span := s.tracer.Start(ctx, "server.listFolderParents") defer span.End() object := zanzana.NewFolderIdent(folderUID) resp, err := s.openfga.Read(ctx, &openfgav1.ReadRequest{ StoreId: store.ID, TupleKey: &openfgav1.ReadRequestTupleKey{ Object: object, Relation: zanzana.RelationParent, }, }) if err != nil { return nil, fmt.Errorf("failed to list folder parents: %w", err) } return resp.Tuples, nil }