summaryrefslogtreecommitdiff
path: root/vendor/github.com/authzed/zed/internal/commands/permission.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/authzed/zed/internal/commands/permission.go')
-rw-r--r--vendor/github.com/authzed/zed/internal/commands/permission.go673
1 files changed, 673 insertions, 0 deletions
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
+}