snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
282
cmd/relationtuple/delete.go
Normal file
282
cmd/relationtuple/delete.go
Normal file
@@ -0,0 +1,282 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user