Files
upstream-mirror 9456a4e922
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:08:34 +05:00

283 lines
7.7 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package relationtuple
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/ory/x/cmdx"
"github.com/spf13/cobra"
"github.com/ory/keto/cmd/client"
"github.com/ory/keto/cmd/helpers"
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
"github.com/ory/keto/ketoapi"
)
const (
MaxFileSizeBytes = 10 * 1024 * 1024 // 10 MB
MaxDirDepth = 3
)
func NewDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <subject_namespace>:<subject_id> <relation> <object_namespace>:<object_id>",
Short: "Delete relationship tuples from inline arguments or JSON files and folders",
Long: `Delete relationship tuples from inline arguments or JSON files and folders.
Inline example:
keto relation-tuple delete User:alice owner Doc:readme
From file or folder:
keto relation-tuple delete -f relationships1.json -f relationships2.json
keto relation-tuple delete -f relationships-dir1 -f relationships-dir2
If a directory is provided, all JSON files inside it are processed.
Use '-' as filename to read from STD_IN:
keto relation-tuple delete -f -`,
Args: cobra.ArbitraryArgs,
RunE: transactTuples(rts.RelationTupleDelta_ACTION_DELETE),
}
registerFileFlag(cmd.Flags())
registerPackageFlags(cmd.Flags())
return cmd
}
func transactTuples(action rts.RelationTupleDelta_Action) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
files, err := cmd.Flags().GetStringSlice(FlagFile)
if err != nil {
return err
}
if len(files) > 0 {
return transactTuplesFromFiles(cmd, action, files)
}
if len(args) != 3 {
return fmt.Errorf("expected inline arguments or JSON files and folders")
}
return transactTupleFromInlineArgs(cmd, action, args)
}
}
func transactTupleFromInlineArgs(cmd *cobra.Command, action rts.RelationTupleDelta_Action, args []string) error {
namespace, object, err := helpers.ParseNamespaceObject(cmd, args[2:])
if err != nil {
return err
}
sub, err := helpers.ParseSubject(args[0])
if err != nil {
return fmt.Errorf("could not parse subject %q: %w", args[0], err)
}
tuple := &rts.RelationTuple{
Namespace: namespace,
Object: object,
Relation: args[1],
Subject: sub,
}
conn, err := client.GetWriteConn(cmd)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
cl := rts.NewWriteServiceClient(conn)
_, err = cl.TransactRelationTuples(cmd.Context(), &rts.TransactRelationTuplesRequest{
RelationTupleDeltas: []*rts.RelationTupleDelta{{
Action: action,
RelationTuple: tuple,
}},
})
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err)
return cmdx.FailSilently(cmd)
}
c, err := NewProtoCollection([]*rts.RelationTuple{tuple})
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "error printing the response: %s\n", err)
}
cmdx.PrintTable(cmd, c)
return nil
}
func transactTuplesFromFiles(cmd *cobra.Command, action rts.RelationTupleDelta_Action, files []string) error {
conn, err := client.GetWriteConn(cmd)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
var tuples []*ketoapi.RelationTuple
var deltas []*rts.RelationTupleDelta
for _, fn := range files {
parsed, err := readTuplesFromPath(cmd, fn, 0, decodeTuples)
if err != nil {
// silencing the usage message as from here on, there can't be any syntax error in the command usage,
// but rather an error with the content of the files
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), err.Error())
return cmdx.FailSilently(cmd)
}
for _, t := range parsed {
tuples = append(tuples, t)
deltas = append(deltas, &rts.RelationTupleDelta{
Action: action,
RelationTuple: t.ToProto(),
})
}
}
cl := rts.NewWriteServiceClient(conn)
_, err = cl.TransactRelationTuples(cmd.Context(), &rts.TransactRelationTuplesRequest{
RelationTupleDeltas: deltas,
})
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err)
return cmdx.FailSilently(cmd)
}
cmdx.PrintTable(cmd, NewAPICollection(tuples))
return nil
}
// TupleDecoder reads relation tuples from a reader. Implementations handle
// format-specific decoding (e.g. JSON, human-readable).
type TupleDecoder func(r io.Reader) ([]*ketoapi.RelationTuple, error)
// readTuplesFromPath reads relation tuples from a file path, directory, or
// stdin (when path is "-"). The depth parameter tracks directory nesting to
// enforce MaxDirDepth. The decode function handles format-specific parsing.
func readTuplesFromPath(cmd *cobra.Command, path string, depth int, decode TupleDecoder) ([]*ketoapi.RelationTuple, error) {
if path == "-" {
tuples, err := decode(cmd.InOrStdin())
if err != nil {
return nil, fmt.Errorf("stdin: %w", err)
}
return tuples, nil
}
cleanPath := filepath.Clean(path)
info, err := os.Stat(cleanPath)
if err != nil {
return nil, fmt.Errorf("error getting stats for %s: %w", path, err)
}
if info.IsDir() {
if depth >= MaxDirDepth {
return nil, fmt.Errorf("maximum directory depth of %d exceeded at %s", MaxDirDepth, path)
}
tuples, err := readTuplesFromDir(cleanPath, decode)
if err != nil {
return nil, err
}
return tuples, nil
}
return readTuplesFromFile(cleanPath, decode)
}
func readTuplesFromDir(dir string, decode TupleDecoder) ([]*ketoapi.RelationTuple, error) {
var tuples []*ketoapi.RelationTuple
fsys := os.DirFS(dir)
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return fmt.Errorf("walking %s: %w", path, walkErr)
}
depth := 0
if path != "." {
depth = strings.Count(path, "/") + 1
}
if depth > MaxDirDepth {
return fmt.Errorf("maximum directory depth of %d exceeded at %s", MaxDirDepth, path)
}
if d.IsDir() {
return nil
}
fullPath := filepath.Join(dir, filepath.FromSlash(path))
t, err := readTuplesFromFile(fullPath, decode)
if err != nil {
return err
}
tuples = append(tuples, t...)
return nil
})
if err != nil {
return nil, err
}
return tuples, nil
}
// readTuplesFromFile opens a single file and decodes its content using the
// provided decoder.
func readTuplesFromFile(path string, decode TupleDecoder) ([]*ketoapi.RelationTuple, error) {
f, err := os.Open(filepath.Clean(path))
if err != nil {
return nil, fmt.Errorf("opening file %s: %w", path, err)
}
defer func() { _ = f.Close() }()
tuples, err := decode(f)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
return tuples, nil
}
// decodeTuples decodes JSON from the reader as either a single RelationTuple
// or an array of RelationTuples. Input is limited to MaxFileSizeBytes.
func decodeTuples(r io.Reader) ([]*ketoapi.RelationTuple, error) {
data, err := io.ReadAll(io.LimitReader(r, MaxFileSizeBytes+1))
if err != nil {
return nil, fmt.Errorf("reading input: %w", err)
}
if len(data) == 0 {
return nil, fmt.Errorf("empty input")
}
if len(data) > MaxFileSizeBytes {
return nil, fmt.Errorf("input exceeds maximum size of %d bytes", MaxFileSizeBytes)
}
// Trim UTF-8 BOM first, then leading/trailing whitespace
trimmed := bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})
trimmed = bytes.TrimSpace(trimmed)
if len(trimmed) == 0 {
return nil, fmt.Errorf("empty input after trimming whitespace")
}
decoder := json.NewDecoder(bytes.NewReader(trimmed))
decoder.DisallowUnknownFields()
if trimmed[0] == '[' {
var tuples []*ketoapi.RelationTuple
if err := decoder.Decode(&tuples); err != nil {
return nil, fmt.Errorf("could not decode: %w", err)
}
return tuples, nil
}
var tuple ketoapi.RelationTuple
if err := decoder.Decode(&tuple); err != nil {
return nil, fmt.Errorf("could not decode: %w", err)
}
return []*ketoapi.RelationTuple{&tuple}, nil
}