summaryrefslogtreecommitdiff
path: root/vendor/github.com/authzed/zed/internal/commands
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/authzed/zed/internal/commands')
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/completion.go165
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/permission.go673
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/relationship.go561
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/relationship_nowasm.go12
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/relationship_wasm.go5
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/schema.go87
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/util.go123
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/watch.go212
8 files changed, 1838 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/zed/internal/commands/completion.go b/vendor/github.com/authzed/zed/internal/commands/completion.go
new file mode 100644
index 0000000..dd24a74
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/completion.go
@@ -0,0 +1,165 @@
+package commands
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+ "github.com/authzed/spicedb/pkg/schemadsl/compiler"
+
+ "github.com/authzed/zed/internal/client"
+)
+
+type CompletionArgumentType int
+
+const (
+ ResourceType CompletionArgumentType = iota
+ ResourceID
+ Permission
+ SubjectType
+ SubjectID
+ SubjectTypeWithOptionalRelation
+)
+
+func FileExtensionCompletions(extension ...string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return extension, cobra.ShellCompDirectiveFilterFileExt
+ }
+}
+
+func GetArgs(fields ...CompletionArgumentType) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ // Read the current schema, if any.
+ schema, err := readSchema(cmd)
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveError
+ }
+
+ // Find the specified resource type, if any.
+ var resourceType string
+ loop:
+ for index, arg := range args {
+ field := fields[index]
+ switch field {
+ case ResourceType:
+ resourceType = arg
+ break loop
+
+ case ResourceID:
+ pieces := strings.Split(arg, ":")
+ if len(pieces) >= 1 {
+ resourceType = pieces[0]
+ break loop
+ }
+ }
+ }
+
+ // Handle : on resource and subject IDs.
+ if strings.HasSuffix(toComplete, ":") && (fields[len(args)] == ResourceID || fields[len(args)] == SubjectID) {
+ comps := []string{}
+ comps = cobra.AppendActiveHelp(comps, "Please enter an object ID")
+ return comps, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ // Handle # on subject types. If the toComplete contains a valid subject,
+ // then we should return the relation names. Note that we cannot do this
+ // on the # character because shell autocompletion won't send it to us.
+ if len(args) == len(fields)-1 && toComplete != "" && fields[len(args)] == SubjectTypeWithOptionalRelation {
+ for _, objDef := range schema.ObjectDefinitions {
+ subjectType := toComplete
+ if objDef.Name == subjectType {
+ relationNames := make([]string, 0)
+ relationNames = append(relationNames, subjectType)
+ for _, relation := range objDef.Relation {
+ relationNames = append(relationNames, subjectType+"#"+relation.Name)
+ }
+ return relationNames, cobra.ShellCompDirectiveNoFileComp
+ }
+ }
+ }
+
+ if len(args) >= len(fields) {
+ // If we have all the arguments, return no completions.
+ return nil, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ // Return the completions.
+ currentFieldType := fields[len(args)]
+ switch currentFieldType {
+ case ResourceType:
+ fallthrough
+
+ case SubjectType:
+ fallthrough
+
+ case SubjectID:
+ fallthrough
+
+ case SubjectTypeWithOptionalRelation:
+ fallthrough
+
+ case ResourceID:
+ resourceTypeNames := make([]string, 0, len(schema.ObjectDefinitions))
+ for _, objDef := range schema.ObjectDefinitions {
+ resourceTypeNames = append(resourceTypeNames, objDef.Name)
+ }
+
+ flags := cobra.ShellCompDirectiveNoFileComp
+ if currentFieldType == ResourceID || currentFieldType == SubjectID || currentFieldType == SubjectTypeWithOptionalRelation {
+ flags |= cobra.ShellCompDirectiveNoSpace
+ }
+
+ return resourceTypeNames, flags
+
+ case Permission:
+ if resourceType == "" {
+ return nil, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ relationNames := make([]string, 0)
+ for _, objDef := range schema.ObjectDefinitions {
+ if objDef.Name == resourceType {
+ for _, relation := range objDef.Relation {
+ relationNames = append(relationNames, relation.Name)
+ }
+ }
+ }
+ return relationNames, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ return nil, cobra.ShellCompDirectiveDefault
+ }
+}
+
+func readSchema(cmd *cobra.Command) (*compiler.CompiledSchema, error) {
+ // TODO: we should find a way to cache this
+ client, err := client.NewClient(cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ request := &v1.ReadSchemaRequest{}
+
+ resp, err := client.ReadSchema(cmd.Context(), request)
+ if err != nil {
+ return nil, err
+ }
+
+ schemaText := resp.SchemaText
+ if len(schemaText) == 0 {
+ return nil, errors.New("no schema defined")
+ }
+
+ compiledSchema, err := compiler.Compile(
+ compiler.InputSchema{Source: "schema", SchemaString: schemaText},
+ compiler.AllowUnprefixedObjectType(),
+ compiler.SkipValidation(),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return compiledSchema, nil
+}
diff --git a/vendor/github.com/authzed/zed/internal/commands/permission.go b/vendor/github.com/authzed/zed/internal/commands/permission.go
new file mode 100644
index 0000000..58b022c
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/permission.go
@@ -0,0 +1,673 @@
+package commands
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/jzelinskie/cobrautil/v2"
+ "github.com/jzelinskie/stringz"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/encoding/prototext"
+
+ "github.com/authzed/authzed-go/pkg/requestmeta"
+ "github.com/authzed/authzed-go/pkg/responsemeta"
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+ "github.com/authzed/spicedb/pkg/tuple"
+
+ "github.com/authzed/zed/internal/client"
+ "github.com/authzed/zed/internal/console"
+ "github.com/authzed/zed/internal/printers"
+)
+
+var ErrMultipleConsistencies = errors.New("provided multiple consistency flags")
+
+func registerConsistencyFlags(flags *pflag.FlagSet) {
+ flags.String("consistency-at-exactly", "", "evaluate at the provided zedtoken")
+ flags.String("consistency-at-least", "", "evaluate at least as consistent as the provided zedtoken")
+ flags.Bool("consistency-min-latency", false, "evaluate at the zedtoken preferred by the database")
+ flags.Bool("consistency-full", false, "evaluate at the newest zedtoken in the database")
+}
+
+func consistencyFromCmd(cmd *cobra.Command) (c *v1.Consistency, err error) {
+ if cobrautil.MustGetBool(cmd, "consistency-full") {
+ c = &v1.Consistency{Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}}
+ }
+ if atLeast := cobrautil.MustGetStringExpanded(cmd, "consistency-at-least"); atLeast != "" {
+ if c != nil {
+ return nil, ErrMultipleConsistencies
+ }
+ c = &v1.Consistency{Requirement: &v1.Consistency_AtLeastAsFresh{AtLeastAsFresh: &v1.ZedToken{Token: atLeast}}}
+ }
+
+ // Deprecated (hidden) flag.
+ if revision := cobrautil.MustGetStringExpanded(cmd, "revision"); revision != "" {
+ if c != nil {
+ return nil, ErrMultipleConsistencies
+ }
+ c = &v1.Consistency{Requirement: &v1.Consistency_AtLeastAsFresh{AtLeastAsFresh: &v1.ZedToken{Token: revision}}}
+ }
+
+ if exact := cobrautil.MustGetStringExpanded(cmd, "consistency-at-exactly"); exact != "" {
+ if c != nil {
+ return nil, ErrMultipleConsistencies
+ }
+ c = &v1.Consistency{Requirement: &v1.Consistency_AtExactSnapshot{AtExactSnapshot: &v1.ZedToken{Token: exact}}}
+ }
+
+ if c == nil {
+ c = &v1.Consistency{Requirement: &v1.Consistency_MinimizeLatency{MinimizeLatency: true}}
+ }
+ return
+}
+
+func RegisterPermissionCmd(rootCmd *cobra.Command) *cobra.Command {
+ rootCmd.AddCommand(permissionCmd)
+
+ permissionCmd.AddCommand(checkCmd)
+ checkCmd.Flags().Bool("json", false, "output as JSON")
+ checkCmd.Flags().String("revision", "", "optional revision at which to check")
+ _ = checkCmd.Flags().MarkHidden("revision")
+ checkCmd.Flags().Bool("explain", false, "requests debug information from SpiceDB and prints out a trace of the requests")
+ checkCmd.Flags().Bool("schema", false, "requests debug information from SpiceDB and prints out the schema used")
+ checkCmd.Flags().Bool("error-on-no-permission", false, "if true, zed will return exit code 1 if subject does not have unconditional permission")
+ checkCmd.Flags().String("caveat-context", "", "the caveat context to send along with the check, in JSON form")
+ registerConsistencyFlags(checkCmd.Flags())
+
+ permissionCmd.AddCommand(checkBulkCmd)
+ checkBulkCmd.Flags().String("revision", "", "optional revision at which to check")
+ checkBulkCmd.Flags().Bool("json", false, "output as JSON")
+ checkBulkCmd.Flags().Bool("explain", false, "requests debug information from SpiceDB and prints out a trace of the requests")
+ checkBulkCmd.Flags().Bool("schema", false, "requests debug information from SpiceDB and prints out the schema used")
+ registerConsistencyFlags(checkBulkCmd.Flags())
+
+ permissionCmd.AddCommand(expandCmd)
+ expandCmd.Flags().Bool("json", false, "output as JSON")
+ expandCmd.Flags().String("revision", "", "optional revision at which to check")
+ registerConsistencyFlags(expandCmd.Flags())
+
+ // NOTE: `lookup` is an alias of `lookup-resources` (below)
+ // and must have the same list of flags in order for it to work.
+ permissionCmd.AddCommand(lookupCmd)
+ lookupCmd.Flags().Bool("json", false, "output as JSON")
+ lookupCmd.Flags().String("revision", "", "optional revision at which to check")
+ lookupCmd.Flags().String("caveat-context", "", "the caveat context to send along with the lookup, in JSON form")
+ lookupCmd.Flags().Uint32("page-limit", 0, "limit of relations returned per page")
+ registerConsistencyFlags(lookupCmd.Flags())
+
+ permissionCmd.AddCommand(lookupResourcesCmd)
+ lookupResourcesCmd.Flags().Bool("json", false, "output as JSON")
+ lookupResourcesCmd.Flags().String("revision", "", "optional revision at which to check")
+ lookupResourcesCmd.Flags().String("caveat-context", "", "the caveat context to send along with the lookup, in JSON form")
+ lookupResourcesCmd.Flags().Uint32("page-limit", 0, "limit of relations returned per page")
+ lookupResourcesCmd.Flags().String("cursor", "", "resume pagination from a specific cursor token")
+ lookupResourcesCmd.Flags().Bool("show-cursor", true, "display the cursor token after pagination")
+ registerConsistencyFlags(lookupResourcesCmd.Flags())
+
+ permissionCmd.AddCommand(lookupSubjectsCmd)
+ lookupSubjectsCmd.Flags().Bool("json", false, "output as JSON")
+ lookupSubjectsCmd.Flags().String("revision", "", "optional revision at which to check")
+ lookupSubjectsCmd.Flags().String("caveat-context", "", "the caveat context to send along with the lookup, in JSON form")
+ registerConsistencyFlags(lookupSubjectsCmd.Flags())
+
+ return permissionCmd
+}
+
+var permissionCmd = &cobra.Command{
+ Use: "permission <subcommand>",
+ Short: "Query the permissions in a permissions system",
+ Aliases: []string{"perm"},
+}
+
+var checkBulkCmd = &cobra.Command{
+ Use: "bulk <resource:id#permission@subject:id> <resource:id#permission@subject:id> ...",
+ Short: "Check a permissions in bulk exists for a resource-subject pairs",
+ Args: ValidationWrapper(cobra.MinimumNArgs(1)),
+ RunE: checkBulkCmdFunc,
+}
+
+var checkCmd = &cobra.Command{
+ Use: "check <resource:id> <permission> <subject:id>",
+ Short: "Check that a permission exists for a subject",
+ Args: ValidationWrapper(cobra.ExactArgs(3)),
+ ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectID),
+ RunE: checkCmdFunc,
+}
+
+var expandCmd = &cobra.Command{
+ Use: "expand <permission> <resource:id>",
+ Short: "Expand the structure of a permission",
+ Args: ValidationWrapper(cobra.ExactArgs(2)),
+ ValidArgsFunction: cobra.NoFileCompletions,
+ RunE: expandCmdFunc,
+}
+
+var lookupResourcesCmd = &cobra.Command{
+ Use: "lookup-resources <type> <permission> <subject:id>",
+ Short: "Enumerates resources of a given type for which the subject has permission",
+ Args: ValidationWrapper(cobra.ExactArgs(3)),
+ ValidArgsFunction: GetArgs(ResourceType, Permission, SubjectID),
+ RunE: lookupResourcesCmdFunc,
+}
+
+var lookupCmd = &cobra.Command{
+ Use: "lookup <type> <permission> <subject:id>",
+ Short: "Enumerates the resources of a given type for which the subject has permission",
+ Args: ValidationWrapper(cobra.ExactArgs(3)),
+ ValidArgsFunction: GetArgs(ResourceType, Permission, SubjectID),
+ RunE: lookupResourcesCmdFunc,
+ Deprecated: "prefer lookup-resources",
+ Hidden: true,
+}
+
+var lookupSubjectsCmd = &cobra.Command{
+ Use: "lookup-subjects <resource:id> <permission> <subject_type#optional_subject_relation>",
+ Short: "Enumerates the subjects of a given type for which the subject has permission on the resource",
+ Args: ValidationWrapper(cobra.ExactArgs(3)),
+ ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation),
+ RunE: lookupSubjectsCmdFunc,
+}
+
+func checkCmdFunc(cmd *cobra.Command, args []string) error {
+ var objectNS, objectID string
+ err := stringz.SplitExact(args[0], ":", &objectNS, &objectID)
+ if err != nil {
+ return err
+ }
+
+ relation := args[1]
+
+ subjectNS, subjectID, subjectRel, err := ParseSubject(args[2])
+ if err != nil {
+ return err
+ }
+
+ caveatContext, err := GetCaveatContext(cmd)
+ if err != nil {
+ return err
+ }
+
+ consistency, err := consistencyFromCmd(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ request := &v1.CheckPermissionRequest{
+ Resource: &v1.ObjectReference{
+ ObjectType: objectNS,
+ ObjectId: objectID,
+ },
+ Permission: relation,
+ Subject: &v1.SubjectReference{
+ Object: &v1.ObjectReference{
+ ObjectType: subjectNS,
+ ObjectId: subjectID,
+ },
+ OptionalRelation: subjectRel,
+ },
+ Context: caveatContext,
+ Consistency: consistency,
+ }
+ log.Trace().Interface("request", request).Send()
+
+ ctx := cmd.Context()
+ if cobrautil.MustGetBool(cmd, "explain") || cobrautil.MustGetBool(cmd, "schema") {
+ log.Info().Msg("debugging requested on check")
+ ctx = requestmeta.AddRequestHeaders(ctx, requestmeta.RequestDebugInformation)
+ request.WithTracing = true
+ }
+
+ var trailerMD metadata.MD
+ resp, err := client.CheckPermission(ctx, request, grpc.Trailer(&trailerMD))
+ if err != nil {
+ var debugInfo *v1.DebugInformation
+
+ // Check for the debug trace contained in the error details.
+ if errInfo, ok := grpcErrorInfoFrom(err); ok {
+ if encodedDebugInfo, ok := errInfo.Metadata["debug_trace_proto_text"]; ok {
+ debugInfo = &v1.DebugInformation{}
+ if uerr := prototext.Unmarshal([]byte(encodedDebugInfo), debugInfo); uerr != nil {
+ return uerr
+ }
+ }
+ }
+
+ derr := displayDebugInformationIfRequested(cmd, debugInfo, trailerMD, true)
+ if derr != nil {
+ return derr
+ }
+
+ return err
+ }
+
+ if cobrautil.MustGetBool(cmd, "json") {
+ prettyProto, err := PrettyProto(resp)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ return nil
+ }
+
+ switch resp.Permissionship {
+ case v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION:
+ log.Warn().Strs("fields", resp.PartialCaveatInfo.MissingRequiredContext).Msg("missing fields in caveat context")
+ console.Println("caveated")
+
+ case v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION:
+ console.Println("true")
+
+ case v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION:
+ console.Println("false")
+
+ default:
+ return fmt.Errorf("unknown permission response: %v", resp.Permissionship)
+ }
+
+ err = displayDebugInformationIfRequested(cmd, resp.DebugTrace, trailerMD, false)
+ if err != nil {
+ return err
+ }
+
+ if cobrautil.MustGetBool(cmd, "error-on-no-permission") {
+ if resp.Permissionship != v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION {
+ os.Exit(1)
+ }
+ }
+
+ return nil
+}
+
+func checkBulkCmdFunc(cmd *cobra.Command, args []string) error {
+ items := make([]*v1.CheckBulkPermissionsRequestItem, 0, len(args))
+ for _, arg := range args {
+ rel, err := tuple.ParseV1Rel(arg)
+ if err != nil {
+ return fmt.Errorf("unable to parse relation: %s", arg)
+ }
+
+ item := &v1.CheckBulkPermissionsRequestItem{
+ Resource: &v1.ObjectReference{
+ ObjectType: rel.Resource.ObjectType,
+ ObjectId: rel.Resource.ObjectId,
+ },
+ Permission: rel.Relation,
+ Subject: &v1.SubjectReference{
+ Object: &v1.ObjectReference{
+ ObjectType: rel.Subject.Object.ObjectType,
+ ObjectId: rel.Subject.Object.ObjectId,
+ },
+ },
+ }
+ if rel.OptionalCaveat != nil {
+ item.Context = rel.OptionalCaveat.Context
+ }
+ items = append(items, item)
+ }
+
+ consistency, err := consistencyFromCmd(cmd)
+ if err != nil {
+ return err
+ }
+
+ bulk := &v1.CheckBulkPermissionsRequest{
+ Consistency: consistency,
+ Items: items,
+ }
+
+ log.Trace().Interface("request", bulk).Send()
+
+ ctx := cmd.Context()
+ c, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ if cobrautil.MustGetBool(cmd, "explain") || cobrautil.MustGetBool(cmd, "schema") {
+ bulk.WithTracing = true
+ }
+
+ resp, err := c.CheckBulkPermissions(ctx, bulk)
+ if err != nil {
+ return err
+ }
+
+ if cobrautil.MustGetBool(cmd, "json") {
+ prettyProto, err := PrettyProto(resp)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ return nil
+ }
+
+ for _, item := range resp.Pairs {
+ console.Printf("%s:%s#%s@%s:%s => ",
+ item.Request.Resource.ObjectType, item.Request.Resource.ObjectId, item.Request.Permission, item.Request.Subject.Object.ObjectType, item.Request.Subject.Object.ObjectId)
+
+ switch responseType := item.Response.(type) {
+ case *v1.CheckBulkPermissionsPair_Item:
+ switch responseType.Item.Permissionship {
+ case v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION:
+ console.Println("caveated")
+
+ case v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION:
+ console.Println("true")
+
+ case v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION:
+ console.Println("false")
+ }
+
+ err = displayDebugInformationIfRequested(cmd, responseType.Item.DebugTrace, nil, false)
+ if err != nil {
+ return err
+ }
+
+ case *v1.CheckBulkPermissionsPair_Error:
+ console.Println(fmt.Sprintf("error: %s", responseType.Error))
+ }
+ }
+
+ return nil
+}
+
+func expandCmdFunc(cmd *cobra.Command, args []string) error {
+ relation := args[0]
+
+ var objectNS, objectID string
+ err := stringz.SplitExact(args[1], ":", &objectNS, &objectID)
+ if err != nil {
+ return err
+ }
+
+ consistency, err := consistencyFromCmd(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ request := &v1.ExpandPermissionTreeRequest{
+ Resource: &v1.ObjectReference{
+ ObjectType: objectNS,
+ ObjectId: objectID,
+ },
+ Permission: relation,
+ Consistency: consistency,
+ }
+ log.Trace().Interface("request", request).Send()
+
+ resp, err := client.ExpandPermissionTree(cmd.Context(), request)
+ if err != nil {
+ return err
+ }
+
+ if cobrautil.MustGetBool(cmd, "json") {
+ prettyProto, err := PrettyProto(resp)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ return nil
+ }
+
+ tp := printers.NewTreePrinter()
+ printers.TreeNodeTree(tp, resp.TreeRoot)
+ tp.Print()
+
+ return nil
+}
+
+var newLookupResourcesPageCallbackForTests func(readByPage uint)
+
+func lookupResourcesCmdFunc(cmd *cobra.Command, args []string) error {
+ objectNS := args[0]
+ relation := args[1]
+ subjectNS, subjectID, subjectRel, err := ParseSubject(args[2])
+ if err != nil {
+ return err
+ }
+
+ pageLimit := cobrautil.MustGetUint32(cmd, "page-limit")
+ caveatContext, err := GetCaveatContext(cmd)
+ if err != nil {
+ return err
+ }
+
+ consistency, err := consistencyFromCmd(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ var cursor *v1.Cursor
+ if cursorStr := cobrautil.MustGetString(cmd, "cursor"); cursorStr != "" {
+ cursor = &v1.Cursor{Token: cursorStr}
+ }
+
+ var totalCount uint
+ for {
+ request := &v1.LookupResourcesRequest{
+ ResourceObjectType: objectNS,
+ Permission: relation,
+ Subject: &v1.SubjectReference{
+ Object: &v1.ObjectReference{
+ ObjectType: subjectNS,
+ ObjectId: subjectID,
+ },
+ OptionalRelation: subjectRel,
+ },
+ Context: caveatContext,
+ Consistency: consistency,
+ OptionalLimit: pageLimit,
+ OptionalCursor: cursor,
+ }
+ log.Trace().Interface("request", request).Uint32("page-limit", pageLimit).Send()
+
+ respStream, err := client.LookupResources(cmd.Context(), request)
+ if err != nil {
+ return err
+ }
+
+ var count uint
+
+ stream:
+ for {
+ resp, err := respStream.Recv()
+ switch {
+ case errors.Is(err, io.EOF):
+ break stream
+ case err != nil:
+ return err
+ default:
+ count++
+ totalCount++
+ if cobrautil.MustGetBool(cmd, "json") {
+ prettyProto, err := PrettyProto(resp)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ }
+
+ console.Println(prettyLookupPermissionship(resp.ResourceObjectId, resp.Permissionship, resp.PartialCaveatInfo))
+ cursor = resp.AfterResultCursor
+ }
+ }
+
+ if newLookupResourcesPageCallbackForTests != nil {
+ newLookupResourcesPageCallbackForTests(count)
+ }
+ if count == 0 || pageLimit == 0 || count < uint(pageLimit) {
+ log.Trace().Interface("request", request).Uint32("page-limit", pageLimit).Uint("count", totalCount).Send()
+ break
+ }
+ }
+
+ showCursor := cobrautil.MustGetBool(cmd, "show-cursor")
+ if showCursor && cursor != nil {
+ console.Printf("Last cursor: %s\n", cursor.Token)
+ }
+
+ return nil
+}
+
+func lookupSubjectsCmdFunc(cmd *cobra.Command, args []string) error {
+ var objectNS, objectID string
+ err := stringz.SplitExact(args[0], ":", &objectNS, &objectID)
+ if err != nil {
+ return err
+ }
+
+ permission := args[1]
+
+ subjectType, subjectRelation := ParseType(args[2])
+
+ caveatContext, err := GetCaveatContext(cmd)
+ if err != nil {
+ return err
+ }
+
+ consistency, err := consistencyFromCmd(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+ request := &v1.LookupSubjectsRequest{
+ Resource: &v1.ObjectReference{
+ ObjectType: objectNS,
+ ObjectId: objectID,
+ },
+ Permission: permission,
+ SubjectObjectType: subjectType,
+ OptionalSubjectRelation: subjectRelation,
+ Context: caveatContext,
+ Consistency: consistency,
+ }
+ log.Trace().Interface("request", request).Send()
+
+ respStream, err := client.LookupSubjects(cmd.Context(), request)
+ if err != nil {
+ return err
+ }
+
+ for {
+ resp, err := respStream.Recv()
+ switch {
+ case errors.Is(err, io.EOF):
+ return nil
+ case err != nil:
+ return err
+ default:
+ if cobrautil.MustGetBool(cmd, "json") {
+ prettyProto, err := PrettyProto(resp)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ }
+ console.Printf("%s:%s%s\n",
+ subjectType,
+ prettyLookupPermissionship(resp.Subject.SubjectObjectId, resp.Subject.Permissionship, resp.Subject.PartialCaveatInfo),
+ excludedSubjectsString(resp.ExcludedSubjects),
+ )
+ }
+ }
+}
+
+func excludedSubjectsString(excluded []*v1.ResolvedSubject) string {
+ if len(excluded) == 0 {
+ return ""
+ }
+
+ var b strings.Builder
+ fmt.Fprintf(&b, " - {\n")
+ for _, subj := range excluded {
+ fmt.Fprintf(&b, "\t%s\n", prettyLookupPermissionship(
+ subj.SubjectObjectId,
+ subj.Permissionship,
+ subj.PartialCaveatInfo,
+ ))
+ }
+ fmt.Fprintf(&b, "}")
+ return b.String()
+}
+
+func prettyLookupPermissionship(objectID string, p v1.LookupPermissionship, info *v1.PartialCaveatInfo) string {
+ var b strings.Builder
+ fmt.Fprint(&b, objectID)
+ if p == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION {
+ fmt.Fprintf(&b, " (caveated, missing context: %s)", strings.Join(info.MissingRequiredContext, ", "))
+ }
+ return b.String()
+}
+
+func displayDebugInformationIfRequested(cmd *cobra.Command, debug *v1.DebugInformation, trailerMD metadata.MD, hasError bool) error {
+ if cobrautil.MustGetBool(cmd, "explain") || cobrautil.MustGetBool(cmd, "schema") {
+ debugInfo := &v1.DebugInformation{}
+ // DebugInformation comes in trailer < 1.30, and in response payload >= 1.30
+ if debug == nil {
+ found, err := responsemeta.GetResponseTrailerMetadataOrNil(trailerMD, responsemeta.DebugInformation)
+ if err != nil {
+ return err
+ }
+
+ if found == nil {
+ log.Warn().Msg("No debugging information returned for the check")
+ return nil
+ }
+
+ err = protojson.Unmarshal([]byte(*found), debugInfo)
+ if err != nil {
+ return err
+ }
+ } else {
+ debugInfo = debug
+ }
+
+ if debugInfo.Check == nil {
+ log.Warn().Msg("No trace found for the check")
+ return nil
+ }
+
+ if cobrautil.MustGetBool(cmd, "explain") {
+ tp := printers.NewTreePrinter()
+ printers.DisplayCheckTrace(debugInfo.Check, tp, hasError)
+ tp.Print()
+ }
+
+ if cobrautil.MustGetBool(cmd, "schema") {
+ console.Println()
+ console.Println(debugInfo.SchemaUsed)
+ }
+ }
+ return nil
+}
diff --git a/vendor/github.com/authzed/zed/internal/commands/relationship.go b/vendor/github.com/authzed/zed/internal/commands/relationship.go
new file mode 100644
index 0000000..0471f85
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/relationship.go
@@ -0,0 +1,561 @@
+package commands
+
+import (
+ "bufio"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "time"
+ "unicode"
+
+ "github.com/jzelinskie/cobrautil/v2"
+ "github.com/jzelinskie/stringz"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "google.golang.org/genproto/googleapis/rpc/errdetails"
+ "google.golang.org/grpc/status"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+ "github.com/authzed/spicedb/pkg/tuple"
+
+ "github.com/authzed/zed/internal/client"
+ "github.com/authzed/zed/internal/console"
+)
+
+func RegisterRelationshipCmd(rootCmd *cobra.Command) *cobra.Command {
+ rootCmd.AddCommand(relationshipCmd)
+
+ relationshipCmd.AddCommand(createCmd)
+ createCmd.Flags().Bool("json", false, "output as JSON")
+ createCmd.Flags().String("caveat", "", `the caveat for the relationship, with format: 'caveat_name:{"some":"context"}'`)
+ createCmd.Flags().String("expiration-time", "", `the expiration time of the relationship in RFC 3339 format`)
+ createCmd.Flags().IntP("batch-size", "b", 100, "batch size when writing streams of relationships from stdin")
+
+ relationshipCmd.AddCommand(touchCmd)
+ touchCmd.Flags().Bool("json", false, "output as JSON")
+ touchCmd.Flags().String("caveat", "", `the caveat for the relationship, with format: 'caveat_name:{"some":"context"}'`)
+ touchCmd.Flags().String("expiration-time", "", `the expiration time for the relationship in RFC 3339 format`)
+ touchCmd.Flags().IntP("batch-size", "b", 100, "batch size when writing streams of relationships from stdin")
+
+ relationshipCmd.AddCommand(deleteCmd)
+ deleteCmd.Flags().Bool("json", false, "output as JSON")
+ deleteCmd.Flags().IntP("batch-size", "b", 100, "batch size when deleting streams of relationships from stdin")
+
+ relationshipCmd.AddCommand(readCmd)
+ readCmd.Flags().Bool("json", false, "output as JSON")
+ readCmd.Flags().String("revision", "", "optional revision at which to check")
+ _ = readCmd.Flags().MarkHidden("revision")
+ readCmd.Flags().String("subject-filter", "", "optional subject filter")
+ readCmd.Flags().Uint32("page-limit", 100, "limit of relations returned per page")
+ registerConsistencyFlags(readCmd.Flags())
+
+ relationshipCmd.AddCommand(bulkDeleteCmd)
+ bulkDeleteCmd.Flags().Bool("force", false, "force deletion of all elements in batches defined by <optional-limit>")
+ bulkDeleteCmd.Flags().String("subject-filter", "", "optional subject filter")
+ bulkDeleteCmd.Flags().Uint32("optional-limit", 1000, "the max amount of elements to delete. If you want to delete all in batches of size <optional-limit>, set --force to true")
+ bulkDeleteCmd.Flags().Bool("estimate-count", true, "estimate the count of relationships to be deleted")
+ _ = bulkDeleteCmd.Flags().MarkDeprecated("estimate-count", "no longer used, make use of --optional-limit instead")
+ return relationshipCmd
+}
+
+var relationshipCmd = &cobra.Command{
+ Use: "relationship <subcommand>",
+ Short: "Query and mutate the relationships in a permissions system",
+}
+
+var createCmd = &cobra.Command{
+ Use: "create <resource:id> <relation> <subject:id#optional_subject_relation>",
+ Short: "Create a relationship for a subject",
+ Args: ValidationWrapper(StdinOrExactArgs(3)),
+ ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation),
+ RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_CREATE, os.Stdin),
+}
+
+var touchCmd = &cobra.Command{
+ Use: "touch <resource:id> <relation> <subject:id#optional_subject_relation>",
+ Short: "Idempotently updates a relationship for a subject",
+ Args: ValidationWrapper(StdinOrExactArgs(3)),
+ ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation),
+ RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_TOUCH, os.Stdin),
+}
+
+var deleteCmd = &cobra.Command{
+ Use: "delete <resource:id> <relation> <subject:id#optional_subject_relation>",
+ Short: "Deletes a relationship",
+ Args: ValidationWrapper(StdinOrExactArgs(3)),
+ ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation),
+ RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_DELETE, os.Stdin),
+}
+
+const readCmdHelpLong = `Enumerates relationships matching the provided pattern.
+
+To filter returned relationships using a resource ID prefix, append a '%' to the resource ID:
+
+zed relationship read some-type:some-prefix-%
+`
+
+var readCmd = &cobra.Command{
+ Use: "read <resource_type:optional_resource_id> <optional_relation> <optional_subject_type:optional_subject_id#optional_subject_relation>",
+ Short: "Enumerates relationships matching the provided pattern",
+ Long: readCmdHelpLong,
+ Args: ValidationWrapper(cobra.RangeArgs(1, 3)),
+ ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation),
+ RunE: readRelationships,
+}
+
+var bulkDeleteCmd = &cobra.Command{
+ Use: "bulk-delete <resource_type:optional_resource_id> <optional_relation> <optional_subject_type:optional_subject_id#optional_subject_relation>",
+ Short: "Deletes relationships matching the provided pattern en masse",
+ Args: ValidationWrapper(cobra.RangeArgs(1, 3)),
+ ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation),
+ RunE: bulkDeleteRelationships,
+}
+
+func StdinOrExactArgs(n int) cobra.PositionalArgs {
+ return func(cmd *cobra.Command, args []string) error {
+ if ok := isArgsViaFile(os.Stdin) && len(args) == 0; ok {
+ return nil
+ }
+
+ return cobra.ExactArgs(n)(cmd, args)
+ }
+}
+
+func isArgsViaFile(file *os.File) bool {
+ return !isFileTerminal(file)
+}
+
+func bulkDeleteRelationships(cmd *cobra.Command, args []string) error {
+ spicedbClient, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ filter, err := buildRelationshipsFilter(cmd, args)
+ if err != nil {
+ return err
+ }
+
+ bar := console.CreateProgressBar("deleting relationships")
+ defer func() {
+ _ = bar.Finish()
+ }()
+
+ allowPartialDeletions := cobrautil.MustGetBool(cmd, "force")
+ optionalLimit := cobrautil.MustGetUint32(cmd, "optional-limit")
+
+ var resp *v1.DeleteRelationshipsResponse
+ for {
+ delRequest := &v1.DeleteRelationshipsRequest{
+ RelationshipFilter: filter,
+ OptionalLimit: optionalLimit,
+ OptionalAllowPartialDeletions: allowPartialDeletions,
+ }
+ log.Trace().Interface("request", delRequest).Msg("deleting relationships")
+
+ resp, err = spicedbClient.DeleteRelationships(cmd.Context(), delRequest)
+ if errorInfo, ok := grpcErrorInfoFrom(err); ok {
+ if errorInfo.GetReason() == v1.ErrorReason_ERROR_REASON_TOO_MANY_RELATIONSHIPS_FOR_TRANSACTIONAL_DELETE.String() {
+ resourceType := "relationships"
+ if returnedResourceType, ok := errorInfo.GetMetadata()["filter_resource_type"]; ok {
+ resourceType = returnedResourceType
+ }
+
+ return fmt.Errorf("could not delete %s, as more than %s relationships were found. Consider increasing --optional-limit or deleting all relationships using --force",
+ resourceType,
+ errorInfo.GetMetadata()["limit"])
+ }
+ }
+ if err != nil {
+ return err
+ }
+
+ if resp.DeletionProgress == v1.DeleteRelationshipsResponse_DELETION_PROGRESS_COMPLETE {
+ break
+ }
+
+ if err := bar.Add(int(optionalLimit)); err != nil {
+ return err
+ }
+ }
+
+ _ = bar.Finish()
+ console.Println(resp.DeletedAt.GetToken())
+ return nil
+}
+
+func grpcErrorInfoFrom(err error) (*errdetails.ErrorInfo, bool) {
+ if err == nil {
+ return nil, false
+ }
+
+ if s, ok := status.FromError(err); ok {
+ for _, d := range s.Details() {
+ if errInfo, ok := d.(*errdetails.ErrorInfo); ok {
+ return errInfo, true
+ }
+ }
+ }
+
+ return nil, false
+}
+
+func buildRelationshipsFilter(cmd *cobra.Command, args []string) (*v1.RelationshipFilter, error) {
+ filter := &v1.RelationshipFilter{ResourceType: args[0]}
+
+ if strings.Contains(args[0], ":") {
+ var resourceID string
+ err := stringz.SplitExact(args[0], ":", &filter.ResourceType, &resourceID)
+ if err != nil {
+ return nil, err
+ }
+
+ if strings.HasSuffix(resourceID, "%") {
+ filter.OptionalResourceIdPrefix = strings.TrimSuffix(resourceID, "%")
+ } else {
+ filter.OptionalResourceId = resourceID
+ }
+ }
+
+ if len(args) > 1 {
+ filter.OptionalRelation = args[1]
+ }
+
+ subjectFilter := cobrautil.MustGetString(cmd, "subject-filter")
+ if len(args) == 3 {
+ if subjectFilter != "" {
+ return nil, errors.New("cannot specify subject filter both positionally and via --subject-filter")
+ }
+ subjectFilter = args[2]
+ }
+
+ if subjectFilter != "" {
+ if strings.Contains(subjectFilter, ":") {
+ subjectNS, subjectID, subjectRel, err := ParseSubject(subjectFilter)
+ if err != nil {
+ return nil, err
+ }
+
+ filter.OptionalSubjectFilter = &v1.SubjectFilter{
+ SubjectType: subjectNS,
+ OptionalSubjectId: subjectID,
+ OptionalRelation: &v1.SubjectFilter_RelationFilter{
+ Relation: subjectRel,
+ },
+ }
+ } else {
+ filter.OptionalSubjectFilter = &v1.SubjectFilter{
+ SubjectType: subjectFilter,
+ }
+ }
+ }
+
+ return filter, nil
+}
+
+func readRelationships(cmd *cobra.Command, args []string) error {
+ spicedbClient, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ filter, err := buildRelationshipsFilter(cmd, args)
+ if err != nil {
+ return err
+ }
+
+ request := &v1.ReadRelationshipsRequest{RelationshipFilter: filter}
+
+ limit := cobrautil.MustGetUint32(cmd, "page-limit")
+ request.OptionalLimit = limit
+ request.Consistency, err = consistencyFromCmd(cmd)
+ if err != nil {
+ return err
+ }
+
+ lastCursor := request.OptionalCursor
+ for {
+ request.OptionalCursor = lastCursor
+ var cursorToken string
+ if lastCursor != nil {
+ cursorToken = lastCursor.Token
+ }
+ log.Trace().Interface("request", request).Str("cursor", cursorToken).Msg("reading relationships page")
+ readRelClient, err := spicedbClient.ReadRelationships(cmd.Context(), request)
+ if err != nil {
+ return err
+ }
+
+ var relCount uint32
+ for {
+ if err := cmd.Context().Err(); err != nil {
+ return err
+ }
+
+ msg, err := readRelClient.Recv()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+
+ if err != nil {
+ return err
+ }
+
+ lastCursor = msg.AfterResultCursor
+ relCount++
+ if err := printRelationship(cmd, msg); err != nil {
+ return err
+ }
+ }
+
+ if relCount < limit || limit == 0 {
+ return nil
+ }
+
+ if relCount > limit {
+ log.Warn().Uint32("limit-specified", limit).Uint32("relationships-received", relCount).Msg("page limit ignored, pagination may not be supported by the server, consider updating SpiceDB")
+ return nil
+ }
+ }
+}
+
+func printRelationship(cmd *cobra.Command, msg *v1.ReadRelationshipsResponse) error {
+ if cobrautil.MustGetBool(cmd, "json") {
+ prettyProto, err := PrettyProto(msg)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ } else {
+ relString, err := relationshipToString(msg.Relationship)
+ if err != nil {
+ return err
+ }
+ console.Println(relString)
+ }
+
+ return nil
+}
+
+func argsToRelationship(args []string) (*v1.Relationship, error) {
+ if len(args) != 3 {
+ return nil, fmt.Errorf("expected 3 arguments, but got %d", len(args))
+ }
+
+ rel, err := tupleToRel(args[0], args[1], args[2])
+ if err != nil {
+ return nil, errors.New("failed to parse input arguments")
+ }
+
+ return rel, nil
+}
+
+func relationshipToString(rel *v1.Relationship) (string, error) {
+ relString, err := tuple.V1StringRelationship(rel)
+ if err != nil {
+ return "", err
+ }
+
+ relString = strings.Replace(relString, "@", " ", 1)
+ relString = strings.Replace(relString, "#", " ", 1)
+ return relString, nil
+}
+
+// parseRelationshipLine splits a line of update input that comes from stdin
+// and returns the fields representing the 3 arguments. This is to handle
+// the fact that relationships specified via stdin can't escape spaces like
+// shell arguments.
+func parseRelationshipLine(line string) (string, string, string, error) {
+ line = strings.TrimSpace(line)
+ resourceIdx := strings.IndexFunc(line, unicode.IsSpace)
+ if resourceIdx == -1 {
+ args := 0
+ if line != "" {
+ args = 1
+ }
+ return "", "", "", fmt.Errorf("expected %s to have 3 arguments, but got %v", line, args)
+ }
+
+ resource := line[:resourceIdx]
+ rest := strings.TrimSpace(line[resourceIdx+1:])
+ relationIdx := strings.IndexFunc(rest, unicode.IsSpace)
+ if relationIdx == -1 {
+ args := 1
+ if strings.TrimSpace(rest) != "" {
+ args = 2
+ }
+ return "", "", "", fmt.Errorf("expected %s to have 3 arguments, but got %v", line, args)
+ }
+
+ relation := rest[:relationIdx]
+ rest = strings.TrimSpace(rest[relationIdx+1:])
+ if rest == "" {
+ return "", "", "", fmt.Errorf("expected %s to have 3 arguments, but got 2", line)
+ }
+
+ return resource, relation, rest, nil
+}
+
+func FileRelationshipParser(f *os.File) RelationshipParser {
+ scanner := bufio.NewScanner(f)
+ return func() (*v1.Relationship, error) {
+ if scanner.Scan() {
+ res, rel, subj, err := parseRelationshipLine(scanner.Text())
+ if err != nil {
+ return nil, err
+ }
+ return tupleToRel(res, rel, subj)
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+ return nil, ErrExhaustedRelationships
+ }
+}
+
+func tupleToRel(resource, relation, subject string) (*v1.Relationship, error) {
+ return tuple.ParseV1Rel(resource + "#" + relation + "@" + subject)
+}
+
+func SliceRelationshipParser(args []string) RelationshipParser {
+ ran := false
+ return func() (*v1.Relationship, error) {
+ if ran {
+ return nil, ErrExhaustedRelationships
+ }
+ ran = true
+ return tupleToRel(args[0], args[1], args[2])
+ }
+}
+
+func writeUpdates(ctx context.Context, spicedbClient client.Client, updates []*v1.RelationshipUpdate, json bool) error {
+ if len(updates) == 0 {
+ return nil
+ }
+ request := &v1.WriteRelationshipsRequest{
+ Updates: updates,
+ OptionalPreconditions: nil,
+ }
+
+ log.Trace().Interface("request", request).Msg("writing relationships")
+ resp, err := spicedbClient.WriteRelationships(ctx, request)
+ if err != nil {
+ return err
+ }
+
+ if json {
+ prettyProto, err := PrettyProto(resp)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ } else {
+ console.Println(resp.WrittenAt.GetToken())
+ }
+
+ return nil
+}
+
+// RelationshipParser is a closure that can produce relationships.
+// When there are no more relationships, it will return ErrExhaustedRelationships.
+type RelationshipParser func() (*v1.Relationship, error)
+
+// ErrExhaustedRelationships signals that the last producible value of a RelationshipParser
+// has already been consumed.
+// Functions should return this error to signal a graceful end of input.
+var ErrExhaustedRelationships = errors.New("exhausted all relationships")
+
+func writeRelationshipCmdFunc(operation v1.RelationshipUpdate_Operation, input *os.File) func(cmd *cobra.Command, args []string) error {
+ return func(cmd *cobra.Command, args []string) error {
+ parser := SliceRelationshipParser(args)
+ if isArgsViaFile(input) && len(args) == 0 {
+ parser = FileRelationshipParser(input)
+ }
+
+ spicedbClient, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ batchSize := cobrautil.MustGetInt(cmd, "batch-size")
+ updateBatch := make([]*v1.RelationshipUpdate, 0)
+ doJSON := cobrautil.MustGetBool(cmd, "json")
+
+ for {
+ rel, err := parser()
+ if errors.Is(err, ErrExhaustedRelationships) {
+ return writeUpdates(cmd.Context(), spicedbClient, updateBatch, doJSON)
+ } else if err != nil {
+ return err
+ }
+
+ if operation != v1.RelationshipUpdate_OPERATION_DELETE {
+ if err := handleCaveatFlag(cmd, rel); err != nil {
+ return err
+ }
+
+ if err := handleExpirationFlag(cmd, rel); err != nil {
+ return err
+ }
+ }
+
+ updateBatch = append(updateBatch, &v1.RelationshipUpdate{
+ Operation: operation,
+ Relationship: rel,
+ })
+ if len(updateBatch) == batchSize {
+ if err := writeUpdates(cmd.Context(), spicedbClient, updateBatch, doJSON); err != nil {
+ return err
+ }
+ updateBatch = nil
+ }
+ }
+ }
+}
+
+func handleCaveatFlag(cmd *cobra.Command, rel *v1.Relationship) error {
+ caveatString := cobrautil.MustGetString(cmd, "caveat")
+ if caveatString != "" {
+ if rel.OptionalCaveat != nil {
+ return errors.New("cannot specify a caveat in both the relationship and the --caveat flag")
+ }
+
+ parts := strings.SplitN(caveatString, ":", 2)
+ if len(parts) == 0 {
+ return fmt.Errorf("invalid --caveat argument. Must be in format `caveat_name:context`, but found `%s`", caveatString)
+ }
+
+ rel.OptionalCaveat = &v1.ContextualizedCaveat{
+ CaveatName: parts[0],
+ }
+
+ if len(parts) == 2 {
+ caveatCtx, err := ParseCaveatContext(parts[1])
+ if err != nil {
+ return err
+ }
+ rel.OptionalCaveat.Context = caveatCtx
+ }
+ }
+ return nil
+}
+
+func handleExpirationFlag(cmd *cobra.Command, rel *v1.Relationship) error {
+ expirationTime := cobrautil.MustGetString(cmd, "expiration-time")
+
+ if expirationTime != "" {
+ t, err := time.Parse(time.RFC3339, expirationTime)
+ if err != nil {
+ return fmt.Errorf("could not parse RFC 3339 timestamp: %w", err)
+ }
+ rel.OptionalExpiresAt = timestamppb.New(t)
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/authzed/zed/internal/commands/relationship_nowasm.go b/vendor/github.com/authzed/zed/internal/commands/relationship_nowasm.go
new file mode 100644
index 0000000..ea94114
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/relationship_nowasm.go
@@ -0,0 +1,12 @@
+//go:build !wasm
+// +build !wasm
+
+package commands
+
+import (
+ "os"
+
+ "golang.org/x/term"
+)
+
+var isFileTerminal = func(f *os.File) bool { return term.IsTerminal(int(f.Fd())) }
diff --git a/vendor/github.com/authzed/zed/internal/commands/relationship_wasm.go b/vendor/github.com/authzed/zed/internal/commands/relationship_wasm.go
new file mode 100644
index 0000000..4388932
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/relationship_wasm.go
@@ -0,0 +1,5 @@
+package commands
+
+import "os"
+
+var isFileTerminal = func(f *os.File) bool { return true }
diff --git a/vendor/github.com/authzed/zed/internal/commands/schema.go b/vendor/github.com/authzed/zed/internal/commands/schema.go
new file mode 100644
index 0000000..b56f152
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/schema.go
@@ -0,0 +1,87 @@
+package commands
+
+import (
+ "context"
+
+ "github.com/jzelinskie/cobrautil/v2"
+ "github.com/jzelinskie/stringz"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+
+ "github.com/authzed/zed/internal/client"
+ "github.com/authzed/zed/internal/console"
+)
+
+func RegisterSchemaCmd(rootCmd *cobra.Command) *cobra.Command {
+ rootCmd.AddCommand(schemaCmd)
+
+ schemaCmd.AddCommand(schemaReadCmd)
+ schemaReadCmd.Flags().Bool("json", false, "output as JSON")
+
+ return schemaCmd
+}
+
+var (
+ schemaCmd = &cobra.Command{
+ Use: "schema <subcommand>",
+ Short: "Manage schema for a permissions system",
+ }
+
+ schemaReadCmd = &cobra.Command{
+ Use: "read",
+ Short: "Read the schema of a permissions system",
+ Args: ValidationWrapper(cobra.ExactArgs(0)),
+ ValidArgsFunction: cobra.NoFileCompletions,
+ RunE: schemaReadCmdFunc,
+ }
+)
+
+func schemaReadCmdFunc(cmd *cobra.Command, _ []string) error {
+ client, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+ request := &v1.ReadSchemaRequest{}
+ log.Trace().Interface("request", request).Msg("requesting schema read")
+
+ resp, err := client.ReadSchema(cmd.Context(), request)
+ if err != nil {
+ return err
+ }
+
+ if cobrautil.MustGetBool(cmd, "json") {
+ prettyProto, err := PrettyProto(resp)
+ if err != nil {
+ return err
+ }
+
+ console.Println(string(prettyProto))
+ return nil
+ }
+
+ console.Println(stringz.Join("\n\n", resp.SchemaText))
+ return nil
+}
+
+// ReadSchema calls read schema for the client and returns the schema found.
+func ReadSchema(ctx context.Context, client client.Client) (string, error) {
+ request := &v1.ReadSchemaRequest{}
+ log.Trace().Interface("request", request).Msg("requesting schema read")
+
+ resp, err := client.ReadSchema(ctx, request)
+ if err != nil {
+ errStatus, ok := status.FromError(err)
+ if !ok || errStatus.Code() != codes.NotFound {
+ return "", err
+ }
+
+ log.Debug().Msg("no schema defined")
+ return "", nil
+ }
+
+ return resp.SchemaText, nil
+}
diff --git a/vendor/github.com/authzed/zed/internal/commands/util.go b/vendor/github.com/authzed/zed/internal/commands/util.go
new file mode 100644
index 0000000..6d5b4da
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/util.go
@@ -0,0 +1,123 @@
+package commands
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/TylerBrock/colorjson"
+ "github.com/jzelinskie/cobrautil/v2"
+ "github.com/jzelinskie/stringz"
+ "github.com/spf13/cobra"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/structpb"
+
+ "github.com/authzed/authzed-go/pkg/requestmeta"
+)
+
+// ParseSubject parses the given subject string into its namespace, object ID
+// and relation, if valid.
+func ParseSubject(s string) (namespace, id, relation string, err error) {
+ err = stringz.SplitExact(s, ":", &namespace, &id)
+ if err != nil {
+ return
+ }
+ err = stringz.SplitExact(id, "#", &id, &relation)
+ if err != nil {
+ relation = ""
+ err = nil
+ }
+ return
+}
+
+// ParseType parses a type reference of the form `namespace#relaion`.
+func ParseType(s string) (namespace, relation string) {
+ namespace, relation, _ = strings.Cut(s, "#")
+ return
+}
+
+// GetCaveatContext returns the entered caveat caveat, if any.
+func GetCaveatContext(cmd *cobra.Command) (*structpb.Struct, error) {
+ contextString := cobrautil.MustGetString(cmd, "caveat-context")
+ if len(contextString) == 0 {
+ return nil, nil
+ }
+
+ return ParseCaveatContext(contextString)
+}
+
+// ParseCaveatContext parses the given context JSON string into caveat context,
+// if valid.
+func ParseCaveatContext(contextString string) (*structpb.Struct, error) {
+ contextMap := map[string]any{}
+ err := json.Unmarshal([]byte(contextString), &contextMap)
+ if err != nil {
+ return nil, fmt.Errorf("invalid caveat context JSON: %w", err)
+ }
+
+ context, err := structpb.NewStruct(contextMap)
+ if err != nil {
+ return nil, fmt.Errorf("could not construct caveat context: %w", err)
+ }
+ return context, err
+}
+
+// PrettyProto returns the given protocol buffer formatted into pretty text.
+func PrettyProto(m proto.Message) ([]byte, error) {
+ encoded, err := protojson.Marshal(m)
+ if err != nil {
+ return nil, err
+ }
+ var obj interface{}
+ err = json.Unmarshal(encoded, &obj)
+ if err != nil {
+ panic("protojson decode failed: " + err.Error())
+ }
+
+ f := colorjson.NewFormatter()
+ f.Indent = 2
+ pretty, err := f.Marshal(obj)
+ if err != nil {
+ panic("colorjson encode failed: " + err.Error())
+ }
+
+ return pretty, nil
+}
+
+// InjectRequestID adds the value of the --request-id flag to the
+// context of the given command.
+func InjectRequestID(cmd *cobra.Command, _ []string) error {
+ ctx := cmd.Context()
+ requestID := cobrautil.MustGetString(cmd, "request-id")
+ if ctx != nil && requestID != "" {
+ cmd.SetContext(requestmeta.WithRequestID(ctx, requestID))
+ }
+
+ return nil
+}
+
+// ValidationError is used to wrap errors that are cobra validation errors. It should be used to
+// wrap the Command.PositionalArgs function in order to be able to determine if the error is a validation error.
+// This is used to determine if an error should print the usage string. Unfortunately Cobra parameter parsing
+// and parameter validation are handled differently, and the latter does not trigger calling Command.FlagErrorFunc
+type ValidationError struct {
+ error
+}
+
+func (ve ValidationError) Is(err error) bool {
+ var validationError ValidationError
+ return errors.As(err, &validationError)
+}
+
+// ValidationWrapper is used to be able to determine if an error is a validation error.
+func ValidationWrapper(f cobra.PositionalArgs) cobra.PositionalArgs {
+ return func(cmd *cobra.Command, args []string) error {
+ if err := f(cmd, args); err != nil {
+ return ValidationError{error: err}
+ }
+
+ return nil
+ }
+}
diff --git a/vendor/github.com/authzed/zed/internal/commands/watch.go b/vendor/github.com/authzed/zed/internal/commands/watch.go
new file mode 100644
index 0000000..96b3451
--- /dev/null
+++ b/vendor/github.com/authzed/zed/internal/commands/watch.go
@@ -0,0 +1,212 @@
+package commands
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/spf13/cobra"
+
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+
+ "github.com/authzed/zed/internal/client"
+ "github.com/authzed/zed/internal/console"
+)
+
+var (
+ watchObjectTypes []string
+ watchRevision string
+ watchTimestamps bool
+ watchRelationshipFilters []string
+)
+
+func RegisterWatchCmd(rootCmd *cobra.Command) *cobra.Command {
+ rootCmd.AddCommand(watchCmd)
+
+ watchCmd.Flags().StringSliceVar(&watchObjectTypes, "object_types", nil, "optional object types to watch updates for")
+ watchCmd.Flags().StringVar(&watchRevision, "revision", "", "optional revision at which to start watching")
+ watchCmd.Flags().BoolVar(&watchTimestamps, "timestamp", false, "shows timestamp of incoming update events")
+ return watchCmd
+}
+
+func RegisterWatchRelationshipCmd(parentCmd *cobra.Command) *cobra.Command {
+ parentCmd.AddCommand(watchRelationshipsCmd)
+ watchRelationshipsCmd.Flags().StringSliceVar(&watchObjectTypes, "object_types", nil, "optional object types to watch updates for")
+ watchRelationshipsCmd.Flags().StringVar(&watchRevision, "revision", "", "optional revision at which to start watching")
+ watchRelationshipsCmd.Flags().BoolVar(&watchTimestamps, "timestamp", false, "shows timestamp of incoming update events")
+ watchRelationshipsCmd.Flags().StringSliceVar(&watchRelationshipFilters, "filter", nil, "optional filter(s) for the watch stream. Example: `optional_resource_type:optional_resource_id_or_prefix#optional_relation@optional_subject_filter`")
+ return watchRelationshipsCmd
+}
+
+var watchCmd = &cobra.Command{
+ Use: "watch [object_types, ...] [start_cursor]",
+ Short: "Watches the stream of relationship updates from the server",
+ Args: ValidationWrapper(cobra.RangeArgs(0, 2)),
+ RunE: watchCmdFunc,
+ Deprecated: "deprecated; please use `zed watch relationships` instead",
+}
+
+var watchRelationshipsCmd = &cobra.Command{
+ Use: "watch [object_types, ...] [start_cursor]",
+ Short: "Watches the stream of relationship updates from the server",
+ Args: ValidationWrapper(cobra.RangeArgs(0, 2)),
+ RunE: watchCmdFunc,
+}
+
+func watchCmdFunc(cmd *cobra.Command, _ []string) error {
+ console.Printf("starting watch stream over types %v and revision %v\n", watchObjectTypes, watchRevision)
+
+ cli, err := client.NewClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ relFilters := make([]*v1.RelationshipFilter, 0, len(watchRelationshipFilters))
+ for _, filter := range watchRelationshipFilters {
+ relFilter, err := parseRelationshipFilter(filter)
+ if err != nil {
+ return err
+ }
+ relFilters = append(relFilters, relFilter)
+ }
+
+ req := &v1.WatchRequest{
+ OptionalObjectTypes: watchObjectTypes,
+ OptionalRelationshipFilters: relFilters,
+ }
+ if watchRevision != "" {
+ req.OptionalStartCursor = &v1.ZedToken{Token: watchRevision}
+ }
+
+ ctx, cancel := context.WithCancel(cmd.Context())
+ defer cancel()
+
+ signalctx, interruptCancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
+ defer interruptCancel()
+
+ watchStream, err := cli.Watch(ctx, req)
+ if err != nil {
+ return err
+ }
+
+ for {
+ select {
+ case <-signalctx.Done():
+ console.Errorf("stream interrupted after program termination\n")
+ return nil
+ case <-ctx.Done():
+ console.Errorf("stream canceled after context cancellation\n")
+ return nil
+ default:
+ resp, err := watchStream.Recv()
+ if err != nil {
+ return err
+ }
+
+ for _, update := range resp.Updates {
+ if watchTimestamps {
+ console.Printf("%v: ", time.Now())
+ }
+
+ switch update.Operation {
+ case v1.RelationshipUpdate_OPERATION_CREATE:
+ console.Printf("CREATED ")
+
+ case v1.RelationshipUpdate_OPERATION_DELETE:
+ console.Printf("DELETED ")
+
+ case v1.RelationshipUpdate_OPERATION_TOUCH:
+ console.Printf("TOUCHED ")
+ }
+
+ subjectRelation := ""
+ if update.Relationship.Subject.OptionalRelation != "" {
+ subjectRelation = " " + update.Relationship.Subject.OptionalRelation
+ }
+
+ console.Printf("%s:%s %s %s:%s%s\n",
+ update.Relationship.Resource.ObjectType,
+ update.Relationship.Resource.ObjectId,
+ update.Relationship.Relation,
+ update.Relationship.Subject.Object.ObjectType,
+ update.Relationship.Subject.Object.ObjectId,
+ subjectRelation,
+ )
+ }
+ }
+ }
+}
+
+func parseRelationshipFilter(relFilterStr string) (*v1.RelationshipFilter, error) {
+ relFilter := &v1.RelationshipFilter{}
+ pieces := strings.Split(relFilterStr, "@")
+ if len(pieces) > 2 {
+ return nil, fmt.Errorf("invalid relationship filter: %s", relFilterStr)
+ }
+
+ if len(pieces) == 2 {
+ subjectFilter, err := parseSubjectFilter(pieces[1])
+ if err != nil {
+ return nil, err
+ }
+ relFilter.OptionalSubjectFilter = subjectFilter
+ }
+
+ if len(pieces) > 0 {
+ resourcePieces := strings.Split(pieces[0], "#")
+ if len(resourcePieces) > 2 {
+ return nil, fmt.Errorf("invalid relationship filter: %s", relFilterStr)
+ }
+
+ if len(resourcePieces) == 2 {
+ relFilter.OptionalRelation = resourcePieces[1]
+ }
+
+ resourceTypePieces := strings.Split(resourcePieces[0], ":")
+ if len(resourceTypePieces) > 2 {
+ return nil, fmt.Errorf("invalid relationship filter: %s", relFilterStr)
+ }
+
+ relFilter.ResourceType = resourceTypePieces[0]
+ if len(resourceTypePieces) == 2 {
+ optionalResourceIDOrPrefix := resourceTypePieces[1]
+ if strings.HasSuffix(optionalResourceIDOrPrefix, "%") {
+ relFilter.OptionalResourceIdPrefix = strings.TrimSuffix(optionalResourceIDOrPrefix, "%")
+ } else {
+ relFilter.OptionalResourceId = optionalResourceIDOrPrefix
+ }
+ }
+ }
+
+ return relFilter, nil
+}
+
+func parseSubjectFilter(subjectFilterStr string) (*v1.SubjectFilter, error) {
+ subjectFilter := &v1.SubjectFilter{}
+ pieces := strings.Split(subjectFilterStr, "#")
+ if len(pieces) > 2 {
+ return nil, fmt.Errorf("invalid subject filter: %s", subjectFilterStr)
+ }
+
+ subjectTypePieces := strings.Split(pieces[0], ":")
+ if len(subjectTypePieces) > 2 {
+ return nil, fmt.Errorf("invalid subject filter: %s", subjectFilterStr)
+ }
+
+ subjectFilter.SubjectType = subjectTypePieces[0]
+ if len(subjectTypePieces) == 2 {
+ subjectFilter.OptionalSubjectId = subjectTypePieces[1]
+ }
+
+ if len(pieces) == 2 {
+ subjectFilter.OptionalRelation = &v1.SubjectFilter_RelationFilter{
+ Relation: pieces[1],
+ }
+ }
+
+ return subjectFilter, nil
+}