diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
| commit | 20ef0d92694465ac86b550df139e8366a0a2b4fa (patch) | |
| tree | 3f14589e1ce6eb9306a3af31c3a1f9e1af5ed637 /vendor/github.com/authzed/zed/internal/commands | |
| parent | 44e0d272c040cdc53a98b9f1dc58ae7da67752e6 (diff) | |
feat: connect to spicedb
Diffstat (limited to 'vendor/github.com/authzed/zed/internal/commands')
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 +} |
