diff --git a/otdfctl/cmd/policy/attributes.go b/otdfctl/cmd/policy/attributes.go index 6847563f5e..35e4000c14 100644 --- a/otdfctl/cmd/policy/attributes.go +++ b/otdfctl/cmd/policy/attributes.go @@ -90,8 +90,9 @@ func listAttributes(cmd *cobra.Command, args []string) { state := cli.GetState(cmd) limit := c.Flags.GetRequiredInt32("limit") offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) - resp, err := h.ListAttributes(cmd.Context(), state, limit, offset) + resp, err := h.ListAttributes(cmd.Context(), state, limit, offset, sort) if err != nil { cli.ExitWithError("Failed to list attributes", err) } @@ -411,6 +412,7 @@ func initAttributesCommands() { listDoc.GetDocFlag("state").Description, ) injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) // Update an attribute updateDoc := man.Docs.GetCommand("policy/attributes/update", diff --git a/otdfctl/cmd/policy/kasKeys.go b/otdfctl/cmd/policy/kasKeys.go index 061907970d..57e0e51471 100644 --- a/otdfctl/cmd/policy/kasKeys.go +++ b/otdfctl/cmd/policy/kasKeys.go @@ -413,6 +413,7 @@ func policyListKasKeys(cmd *cobra.Command, args []string) { if err != nil { cli.ExitWithError("Invalid legacy flag", err) } + sort := getSortOption(c) kasLookup, err := resolveKasIdentifier(kasIdentifier) if err != nil { @@ -420,7 +421,7 @@ func policyListKasKeys(cmd *cobra.Command, args []string) { } // Get the list of keys. - resp, err := h.ListKasKeys(c.Context(), limit, offset, alg, kasLookup, legacy) + resp, err := h.ListKasKeys(c.Context(), limit, offset, alg, kasLookup, legacy, sort) if err != nil { cli.ExitWithError("Failed to list kas keys", err) } @@ -930,6 +931,7 @@ func initKASKeysCommands() { listDoc.GetDocFlag("legacy").Description, ) injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) // Rotate Kas Key rotateDoc := man.Docs.GetCommand("policy/kas-registry/key/rotate", diff --git a/otdfctl/cmd/policy/kasRegistry.go b/otdfctl/cmd/policy/kasRegistry.go index 006c9fecab..4b5884cfa5 100644 --- a/otdfctl/cmd/policy/kasRegistry.go +++ b/otdfctl/cmd/policy/kasRegistry.go @@ -61,8 +61,9 @@ func listKeyAccessRegistries(cmd *cobra.Command, args []string) { limit := c.Flags.GetRequiredInt32("limit") offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) - resp, err := h.ListKasRegistryEntries(cmd.Context(), limit, offset) + resp, err := h.ListKasRegistryEntries(cmd.Context(), limit, offset, sort) if err != nil { cli.ExitWithError("Failed to list Registered KAS entries", err) } @@ -226,6 +227,7 @@ func initKASRegistryCommands() { man.WithRun(listKeyAccessRegistries), ) injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) createDoc := man.Docs.GetCommand("policy/kas-registry/create", man.WithRun(createKeyAccessRegistry), diff --git a/otdfctl/cmd/policy/namespaces.go b/otdfctl/cmd/policy/namespaces.go index 2605845c02..b4f741cdf8 100644 --- a/otdfctl/cmd/policy/namespaces.go +++ b/otdfctl/cmd/policy/namespaces.go @@ -45,8 +45,9 @@ func listAttributeNamespaces(cmd *cobra.Command, args []string) { state := cli.GetState(cmd) limit := c.Flags.GetRequiredInt32("limit") offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) - resp, err := h.ListNamespaces(cmd.Context(), state, limit, offset) + resp, err := h.ListNamespaces(cmd.Context(), state, limit, offset, sort) if err != nil { cli.ExitWithError("Failed to list namespaces", err) } @@ -316,128 +317,107 @@ func policyRemoveKeyFromNamespace(cmd *cobra.Command, args []string) { common.HandleSuccess(cmd, namespace, t, nil) } -// newCommandFromDoc creates an independent cobra.Command with metadata copied from a Doc. -// This allows registering the same logical command under multiple parents. -func newCommandFromDoc(doc *man.Doc, run func(*cobra.Command, []string)) *cobra.Command { - cmd := &cobra.Command{ - Use: doc.Use, - Short: doc.Short, - Long: doc.Long, - Args: doc.Args, - Aliases: doc.Aliases, - Hidden: doc.Hidden, - Run: run, - } - return cmd -} - -// buildNamespacesCommandTree creates a full namespaces command tree with all subcommands and flags. -// Each call returns an independent *cobra.Command so it can be parented under multiple commands. -func buildNamespacesCommandTree() *cobra.Command { - nsDoc := man.Docs.GetDoc("policy/namespaces") - nsCmd := newCommandFromDoc(nsDoc, nil) +func initNamespacesCommands() { + nsDoc := man.Docs.GetCommand("policy/namespaces") - getDoc := man.Docs.GetDoc("policy/namespaces/get") - getCmd := newCommandFromDoc(getDoc, getAttributeNamespace) - getCmd.Flags().StringP( + getDoc := man.Docs.GetCommand("policy/namespaces/get", + man.WithRun(getAttributeNamespace), + ) + getDoc.Flags().StringP( getDoc.GetDocFlag("id").Name, getDoc.GetDocFlag("id").Shorthand, getDoc.GetDocFlag("id").Default, getDoc.GetDocFlag("id").Description, ) - listDoc := man.Docs.GetDoc("policy/namespaces/list") - listCmd := newCommandFromDoc(listDoc, listAttributeNamespaces) - listCmd.Flags().StringP( + listDoc := man.Docs.GetCommand("policy/namespaces/list", + man.WithRun(listAttributeNamespaces), + ) + listDoc.Flags().StringP( listDoc.GetDocFlag("state").Name, listDoc.GetDocFlag("state").Shorthand, listDoc.GetDocFlag("state").Default, listDoc.GetDocFlag("state").Description, ) - listCmd.Flags().Int32P( - listDoc.GetDocFlag("limit").Name, - listDoc.GetDocFlag("limit").Shorthand, - defaultListFlagLimit, - listDoc.GetDocFlag("limit").Description, - ) - listCmd.Flags().Int32P( - listDoc.GetDocFlag("offset").Name, - listDoc.GetDocFlag("offset").Shorthand, - defaultListFlagOffset, - listDoc.GetDocFlag("offset").Description, - ) + injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) - createDoc := man.Docs.GetDoc("policy/namespaces/create") - createCmd := newCommandFromDoc(createDoc, createAttributeNamespace) - createCmd.Flags().StringP( + createDoc := man.Docs.GetCommand("policy/namespaces/create", + man.WithRun(createAttributeNamespace), + ) + createDoc.Flags().StringP( createDoc.GetDocFlag("name").Name, createDoc.GetDocFlag("name").Shorthand, createDoc.GetDocFlag("name").Default, createDoc.GetDocFlag("name").Description, ) - injectLabelFlags(createCmd, false) + injectLabelFlags(&createDoc.Command, false) - updateDoc := man.Docs.GetDoc("policy/namespaces/update") - updateCmd := newCommandFromDoc(updateDoc, updateAttributeNamespace) - updateCmd.Flags().StringP( + updateDoc := man.Docs.GetCommand("policy/namespaces/update", + man.WithRun(updateAttributeNamespace), + ) + updateDoc.Flags().StringP( updateDoc.GetDocFlag("id").Name, updateDoc.GetDocFlag("id").Shorthand, updateDoc.GetDocFlag("id").Default, updateDoc.GetDocFlag("id").Description, ) - injectLabelFlags(updateCmd, true) + injectLabelFlags(&updateDoc.Command, true) - deactivateDoc := man.Docs.GetDoc("policy/namespaces/deactivate") - deactivateCmd := newCommandFromDoc(deactivateDoc, deactivateAttributeNamespace) - deactivateCmd.Flags().StringP( + deactivateDoc := man.Docs.GetCommand("policy/namespaces/deactivate", + man.WithRun(deactivateAttributeNamespace), + ) + deactivateDoc.Flags().StringP( deactivateDoc.GetDocFlag("id").Name, deactivateDoc.GetDocFlag("id").Shorthand, deactivateDoc.GetDocFlag("id").Default, deactivateDoc.GetDocFlag("id").Description, ) - deactivateCmd.Flags().Bool( + deactivateDoc.Flags().Bool( deactivateDoc.GetDocFlag("force").Name, false, deactivateDoc.GetDocFlag("force").Description, ) // unsafe - unsafeDoc := man.Docs.GetDoc("policy/namespaces/unsafe") - unsafeCmd := newCommandFromDoc(unsafeDoc, nil) - unsafeCmd.PersistentFlags().BoolVar( + unsafeDoc := man.Docs.GetCommand("policy/namespaces/unsafe") + unsafeDoc.PersistentFlags().BoolVar( &forceUnsafe, unsafeDoc.GetDocFlag("force").Name, false, unsafeDoc.GetDocFlag("force").Description, ) - deleteDoc := man.Docs.GetDoc("policy/namespaces/unsafe/delete") - deleteCmd := newCommandFromDoc(deleteDoc, unsafeDeleteAttributeNamespace) - deleteCmd.Flags().StringP( + deleteDoc := man.Docs.GetCommand("policy/namespaces/unsafe/delete", + man.WithRun(unsafeDeleteAttributeNamespace), + ) + deleteDoc.Flags().StringP( deleteDoc.GetDocFlag("id").Name, deleteDoc.GetDocFlag("id").Shorthand, deleteDoc.GetDocFlag("id").Default, deleteDoc.GetDocFlag("id").Description, ) - reactivateDoc := man.Docs.GetDoc("policy/namespaces/unsafe/reactivate") - reactivateCmd := newCommandFromDoc(reactivateDoc, unsafeReactivateAttributeNamespace) - reactivateCmd.Flags().StringP( + reactivateDoc := man.Docs.GetCommand("policy/namespaces/unsafe/reactivate", + man.WithRun(unsafeReactivateAttributeNamespace), + ) + reactivateDoc.Flags().StringP( reactivateDoc.GetDocFlag("id").Name, reactivateDoc.GetDocFlag("id").Shorthand, reactivateDoc.GetDocFlag("id").Default, reactivateDoc.GetDocFlag("id").Description, ) - unsafeUpdateDoc := man.Docs.GetDoc("policy/namespaces/unsafe/update") - unsafeUpdateCmd := newCommandFromDoc(unsafeUpdateDoc, unsafeUpdateAttributeNamespace) - unsafeUpdateCmd.Flags().StringP( + unsafeUpdateDoc := man.Docs.GetCommand("policy/namespaces/unsafe/update", + man.WithRun(unsafeUpdateAttributeNamespace), + ) + unsafeUpdateDoc.Flags().StringP( unsafeUpdateDoc.GetDocFlag("id").Name, unsafeUpdateDoc.GetDocFlag("id").Shorthand, unsafeUpdateDoc.GetDocFlag("id").Default, unsafeUpdateDoc.GetDocFlag("id").Description, ) - unsafeUpdateCmd.Flags().StringP( + unsafeUpdateDoc.Flags().StringP( unsafeUpdateDoc.GetDocFlag("name").Name, unsafeUpdateDoc.GetDocFlag("name").Shorthand, unsafeUpdateDoc.GetDocFlag("name").Default, @@ -445,47 +425,43 @@ func buildNamespacesCommandTree() *cobra.Command { ) // key - keyDoc := man.Docs.GetDoc("policy/namespaces/key") - keyCmd := newCommandFromDoc(keyDoc, nil) + keyDoc := man.Docs.GetCommand("policy/namespaces/key") - assignDoc := man.Docs.GetDoc("policy/namespaces/key/assign") - assignCmd := newCommandFromDoc(assignDoc, policyAssignKeyToNamespace) - assignCmd.Flags().StringP( + assignDoc := man.Docs.GetCommand("policy/namespaces/key/assign", + man.WithRun(policyAssignKeyToNamespace), + ) + assignDoc.Flags().StringP( assignDoc.GetDocFlag("namespace").Name, assignDoc.GetDocFlag("namespace").Shorthand, assignDoc.GetDocFlag("namespace").Default, assignDoc.GetDocFlag("namespace").Description, ) - assignCmd.Flags().StringP( + assignDoc.Flags().StringP( assignDoc.GetDocFlag("key-id").Name, assignDoc.GetDocFlag("key-id").Shorthand, assignDoc.GetDocFlag("key-id").Default, assignDoc.GetDocFlag("key-id").Description, ) - removeDoc := man.Docs.GetDoc("policy/namespaces/key/remove") - removeCmd := newCommandFromDoc(removeDoc, policyRemoveKeyFromNamespace) - removeCmd.Flags().StringP( + removeDoc := man.Docs.GetCommand("policy/namespaces/key/remove", + man.WithRun(policyRemoveKeyFromNamespace), + ) + removeDoc.Flags().StringP( removeDoc.GetDocFlag("namespace").Name, removeDoc.GetDocFlag("namespace").Shorthand, removeDoc.GetDocFlag("namespace").Default, removeDoc.GetDocFlag("namespace").Description, ) - removeCmd.Flags().StringP( + removeDoc.Flags().StringP( removeDoc.GetDocFlag("key-id").Name, removeDoc.GetDocFlag("key-id").Shorthand, removeDoc.GetDocFlag("key-id").Default, removeDoc.GetDocFlag("key-id").Description, ) - keyCmd.AddCommand(assignCmd, removeCmd) - unsafeCmd.AddCommand(deleteCmd, reactivateCmd, unsafeUpdateCmd) - nsCmd.AddCommand(getCmd, listCmd, createCmd, updateCmd, deactivateCmd, unsafeCmd, keyCmd) - - return nsCmd -} - -func initNamespacesCommands() { - Cmd.AddCommand(buildNamespacesCommandTree()) - AttributesCmd.AddCommand(buildNamespacesCommandTree()) + keyDoc.AddSubcommands(assignDoc, removeDoc) + unsafeDoc.AddSubcommands(deleteDoc, reactivateDoc, unsafeUpdateDoc) + nsDoc.AddSubcommands(getDoc, listDoc, createDoc, updateDoc, deactivateDoc, unsafeDoc, keyDoc) + AttributesCmd.AddCommand(&nsDoc.Command) + Cmd.AddCommand(&nsDoc.Command) } diff --git a/otdfctl/cmd/policy/obligations.go b/otdfctl/cmd/policy/obligations.go index 3d4e0b1148..d8252be874 100644 --- a/otdfctl/cmd/policy/obligations.go +++ b/otdfctl/cmd/policy/obligations.go @@ -99,8 +99,9 @@ func policyListObligations(cmd *cobra.Command, args []string) { namespace := c.Flags.GetOptionalString("namespace") limit := c.Flags.GetRequiredInt32("limit") offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) - resp, err := h.ListObligations(cmd.Context(), limit, offset, namespace) + resp, err := h.ListObligations(cmd.Context(), limit, offset, namespace, sort) if err != nil { cli.ExitWithError("Failed to list obligations", err) } @@ -525,6 +526,7 @@ func initObligationsCommands() { listDoc.GetDocFlag("namespace").Description, ) injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) createDoc := man.Docs.GetCommand("policy/obligations/create", man.WithRun(policyCreateObligation), diff --git a/otdfctl/cmd/policy/policy.go b/otdfctl/cmd/policy/policy.go index 28d2ab5e31..49e3d40195 100644 --- a/otdfctl/cmd/policy/policy.go +++ b/otdfctl/cmd/policy/policy.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/opentdf/platform/otdfctl/pkg/cli" + "github.com/opentdf/platform/otdfctl/pkg/handlers" "github.com/opentdf/platform/otdfctl/pkg/man" "github.com/opentdf/platform/protocol/go/common" "github.com/spf13/cobra" @@ -85,6 +86,22 @@ func injectListPaginationFlags(listDoc *man.Doc) { ) } +func injectListSortFlags(listDoc *man.Doc) { + sortFlag := listDoc.GetDocFlag("sort") + listDoc.Flags().String(sortFlag.Name, sortFlag.Default, sortFlag.Description) + + orderFlag := listDoc.GetDocFlag("order") + listDoc.Flags().String(orderFlag.Name, orderFlag.Default, orderFlag.Description) +} + +func getSortOption(c *cli.Cli) handlers.SortOption { + sort, err := handlers.NewSortOption(c.Flags.GetOptionalString("sort"), c.Flags.GetOptionalString("order")) + if err != nil { + cli.ExitWithError("Invalid sort order", err) + } + return sort +} + func InitCommands() { initActionsCommands() initAttributesCommands() diff --git a/otdfctl/cmd/policy/registeredResources.go b/otdfctl/cmd/policy/registeredResources.go index e33cf7cd04..c4a026f836 100644 --- a/otdfctl/cmd/policy/registeredResources.go +++ b/otdfctl/cmd/policy/registeredResources.go @@ -103,8 +103,9 @@ func policyListRegisteredResources(cmd *cobra.Command, args []string) { namespace := c.Flags.GetOptionalString("namespace") limit := c.Flags.GetRequiredInt32("limit") offset := c.Flags.GetRequiredInt32("offset") + sort := getSortOption(c) - resp, err := h.ListRegisteredResources(cmd.Context(), limit, offset, namespace) + resp, err := h.ListRegisteredResources(cmd.Context(), limit, offset, namespace, sort) if err != nil { cli.ExitWithError("Failed to list registered resources", err) } @@ -492,6 +493,7 @@ func initRegisteredResourcesCommands() { listDoc.GetDocFlag("namespace").Description, ) injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) createDoc := man.Docs.GetCommand("policy/registered-resources/create", man.WithRun(policyCreateRegisteredResource), diff --git a/otdfctl/cmd/policy/subjectConditionSets.go b/otdfctl/cmd/policy/subjectConditionSets.go index d117ce7e72..8344e9d6ab 100644 --- a/otdfctl/cmd/policy/subjectConditionSets.go +++ b/otdfctl/cmd/policy/subjectConditionSets.go @@ -150,8 +150,9 @@ func listSubjectConditionSets(cmd *cobra.Command, args []string) { limit := c.Flags.GetRequiredInt32("limit") offset := c.Flags.GetRequiredInt32("offset") namespace := c.Flags.GetOptionalString("namespace") + sort := getSortOption(c) - resp, err := h.ListSubjectConditionSets(cmd.Context(), limit, offset, namespace) + resp, err := h.ListSubjectConditionSets(cmd.Context(), limit, offset, namespace, sort) if err != nil { cli.ExitWithError("Error listing subject condition sets", err) } @@ -360,6 +361,7 @@ func initSubjectConditionSetsCommands() { man.WithRun(listSubjectConditionSets), ) injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) listDoc.Flags().StringP( listDoc.GetDocFlag("namespace").Name, listDoc.GetDocFlag("namespace").Shorthand, diff --git a/otdfctl/cmd/policy/subjectMappings.go b/otdfctl/cmd/policy/subjectMappings.go index 55c133b1d2..0317148b39 100644 --- a/otdfctl/cmd/policy/subjectMappings.go +++ b/otdfctl/cmd/policy/subjectMappings.go @@ -67,8 +67,9 @@ func policyListSubjectMappings(cmd *cobra.Command, args []string) { limit := c.Flags.GetRequiredInt32("limit") offset := c.Flags.GetRequiredInt32("offset") namespace := c.Flags.GetOptionalString("namespace") + sort := getSortOption(c) - resp, err := h.ListSubjectMappings(cmd.Context(), limit, offset, namespace) + resp, err := h.ListSubjectMappings(cmd.Context(), limit, offset, namespace, sort) if err != nil { cli.ExitWithError("Failed to get subject mappings", err) } @@ -340,6 +341,7 @@ func initSubjectMappingsCommands() { man.WithRun(policyListSubjectMappings), ) injectListPaginationFlags(listDoc) + injectListSortFlags(listDoc) listDoc.Flags().StringP( listDoc.GetDocFlag("namespace").Name, listDoc.GetDocFlag("namespace").Shorthand, diff --git a/otdfctl/docs/man/policy/attributes/list.md b/otdfctl/docs/man/policy/attributes/list.md index 8e3af71d5e..c3972d6ca0 100644 --- a/otdfctl/docs/man/policy/attributes/list.md +++ b/otdfctl/docs/man/policy/attributes/list.md @@ -19,14 +19,51 @@ command: - name: offset shorthand: o description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- By default, the list will only provide `active` attributes if unspecified, but the filter can be controlled with the `--state` flag. For more general information about attributes, see the `attributes` subcommand. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Attribute name | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy attributes list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy attributes list --order asc +``` + ## Example ```shell otdfctl policy attributes list ``` + +Sort attributes by name ascending: + +```shell +otdfctl policy attributes list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/kas-registry/key/list.md b/otdfctl/docs/man/policy/kas-registry/key/list.md index dcb34f1cae..99beaf84ef 100644 --- a/otdfctl/docs/man/policy/kas-registry/key/list.md +++ b/otdfctl/docs/man/policy/kas-registry/key/list.md @@ -21,12 +21,43 @@ command: - name: legacy description: Filter keys by legacy status. required: false + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- This command lists keys registered within a specified Key Access Server (KAS). You must specify the KAS using its ID, URI, or Name. The list can be filtered by key algorithm. Pagination is supported using `limit` and `offset` flags to manage the number of results returned. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `key_id` | Key ID | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy kas-registry key list --kas "https://kas.example.com/kas" --sort key_id +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy kas-registry key list --kas "https://kas.example.com/kas" --order asc +``` + ## Examples List the first 10 keys from a KAS specified by its URI: @@ -58,3 +89,9 @@ Exclude legacy keys ```shell otdfctl policy kas-registry key list --legacy false ``` + +Sort keys by key ID descending: + +```shell +otdfctl policy kas-registry key list --kas "https://kas.example.com/kas" --sort key_id --order desc +``` diff --git a/otdfctl/docs/man/policy/kas-registry/list.md b/otdfctl/docs/man/policy/kas-registry/list.md index 6eb8581b7a..ab51a17bf0 100644 --- a/otdfctl/docs/man/policy/kas-registry/list.md +++ b/otdfctl/docs/man/policy/kas-registry/list.md @@ -11,12 +11,50 @@ command: - name: offset shorthand: o description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- For more information about registration of Key Access Servers, see the manual for `kas-registry`. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | KAS registration name | No | +| `uri` | KAS URI | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy kas-registry list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy kas-registry list --order asc +``` + ## Example ```shell otdfctl policy kas-registry list ``` + +Sort KAS registrations by URI descending: + +```shell +otdfctl policy kas-registry list --sort uri --order desc +``` diff --git a/otdfctl/docs/man/policy/namespaces/list.md b/otdfctl/docs/man/policy/namespaces/list.md index 390d84a5f8..8fa3e0802d 100644 --- a/otdfctl/docs/man/policy/namespaces/list.md +++ b/otdfctl/docs/man/policy/namespaces/list.md @@ -15,12 +15,50 @@ command: - name: offset shorthand: o description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- For more general information, see the `namespaces` subcommand. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Namespace name | No | +| `fqn` | Namespace FQN | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy namespaces list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy namespaces list --order asc +``` + ## Example ```shell otdfctl policy namespaces list ``` + +Sort namespaces by name ascending: + +```shell +otdfctl policy namespaces list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/obligations/list.md b/otdfctl/docs/man/policy/obligations/list.md index f497413140..3cae7e32e8 100644 --- a/otdfctl/docs/man/policy/obligations/list.md +++ b/otdfctl/docs/man/policy/obligations/list.md @@ -14,14 +14,52 @@ command: - name: namespace shorthand: n description: Namespace ID or FQN by which to filter results + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- List obligations definitions (optionally by namespace). For more information about obligations, see the `obligations` subcommand. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Obligation name | No | +| `fqn` | Obligation FQN | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy obligations list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy obligations list --order asc +``` + ## Example ```shell otdfctl policy obligations list --limit 10 --offset 0 ``` + +Sort obligations by name ascending: + +```shell +otdfctl policy obligations list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/registered-resources/list.md b/otdfctl/docs/man/policy/registered-resources/list.md index ab7649a16f..a51ee69357 100644 --- a/otdfctl/docs/man/policy/registered-resources/list.md +++ b/otdfctl/docs/man/policy/registered-resources/list.md @@ -14,12 +14,49 @@ command: - name: offset shorthand: o description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- For more information about Registered Resources, see the `registered-resources` subcommand. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `name` | Registered resource name | No | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy registered-resources list --sort name +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy registered-resources list --order asc +``` + ## Example ```shell otdfctl policy registered-resources list ``` + +Sort registered resources by name ascending: + +```shell +otdfctl policy registered-resources list --sort name --order asc +``` diff --git a/otdfctl/docs/man/policy/subject-condition-sets/list.md b/otdfctl/docs/man/policy/subject-condition-sets/list.md index 1d489b84f6..794fb17306 100644 --- a/otdfctl/docs/man/policy/subject-condition-sets/list.md +++ b/otdfctl/docs/man/policy/subject-condition-sets/list.md @@ -15,14 +15,50 @@ command: - name: offset shorthand: o description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- For more information about subject condition sets, see the `subject-condition-sets` subcommand. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy subject-condition-sets list --sort created_at +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy subject-condition-sets list --order asc +``` + ## Example ```shell -otdfctl policy subject-condition-set list +otdfctl policy subject-condition-sets list -otdfctl policy subject-condition-set list --namespace "https://example.com" +otdfctl policy subject-condition-sets list --namespace https://example.com +``` + +Sort subject condition sets by creation time ascending: + +```shell +otdfctl policy subject-condition-sets list --sort created_at --order asc ``` diff --git a/otdfctl/docs/man/policy/subject-mappings/list.md b/otdfctl/docs/man/policy/subject-mappings/list.md index 2a65e61c89..359b3915cc 100644 --- a/otdfctl/docs/man/policy/subject-mappings/list.md +++ b/otdfctl/docs/man/policy/subject-mappings/list.md @@ -14,10 +14,40 @@ command: - name: offset shorthand: o description: Offset (page) quantity from start of the list + - name: sort + description: Sort list results by field + - name: order + description: Sort order direction. Accepted values are asc and desc --- For more information about subject mappings, see the `subject-mappings` subcommand. +## Sort Options + +Use `--sort ` with optional `--order `. Either flag may be omitted. + +| Direction | Description | Default | +| --- | --- | --- | +| `asc` | Ascending order | No | +| `desc` | Descending order | Yes | + +| Field | Description | Default | +| --- | --- | --- | +| `created_at` | Creation timestamp | Yes | +| `updated_at` | Last update timestamp | No | + +Omit direction and let the server choose the default direction: + +```shell +otdfctl policy subject-mappings list --sort created_at +``` + +Omit field and let the server choose the default field: + +```shell +otdfctl policy subject-mappings list --order asc +``` + ## Example ```shell @@ -25,3 +55,9 @@ otdfctl policy subject-mappings list otdfctl policy subject-mappings list --namespace "https://example.com" ``` + +Sort subject mappings by creation time ascending: + +```shell +otdfctl policy subject-mappings list --sort created_at --order asc +``` diff --git a/otdfctl/e2e/attributes.bats b/otdfctl/e2e/attributes.bats index 6bd50deaed..5d0680d7e9 100755 --- a/otdfctl/e2e/attributes.bats +++ b/otdfctl/e2e/attributes.bats @@ -146,6 +146,48 @@ teardown_file() { assert_line --regexp "Current Offset.*0" } +@test "List attribute definitions supports sort and order flags" { + sort_prefix="sort_attr_${BATS_TEST_NUMBER}_$RANDOM" + attr_a_id=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name "${sort_prefix}_alpha" --rule ANY_OF --json | jq -r '.id') + attr_b_id=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name "${sort_prefix}_bravo" --rule ANY_OF --json | jq -r '.id') + attr_c_id=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name "${sort_prefix}_charlie" --rule ANY_OF --json | jq -r '.id') + + run_otdfctl_attr list --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.attributes[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$attr_a_id,$attr_b_id,$attr_c_id" + + run_otdfctl_attr list --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.attributes[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$attr_c_id,$attr_b_id,$attr_a_id" + + run_otdfctl_attr list --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$attr_a_id" --arg b "$attr_b_id" --arg c "$attr_c_id" '[.attributes[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$attr_a_id,$attr_b_id,$attr_c_id" + + run_otdfctl_attr update --id "$attr_a_id" --label sort=a --json + assert_success + run_otdfctl_attr update --id "$attr_b_id" --label sort=b --json + assert_success + run_otdfctl_attr update --id "$attr_c_id" --label sort=c --json + assert_success + + run_otdfctl_attr list --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$attr_a_id" --arg b "$attr_b_id" --arg c "$attr_c_id" '[.attributes[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$attr_a_id,$attr_b_id,$attr_c_id" + + run_otdfctl_attr list --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.attributes[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$attr_c_id,$attr_b_id,$attr_a_id" + + run_otdfctl_attr list --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$attr_a_id" --arg b "$attr_b_id" --arg c "$attr_c_id" '[.attributes[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$attr_a_id,$attr_b_id,$attr_c_id" + + run_otdfctl_attr unsafe delete --force --id "$attr_a_id" + run_otdfctl_attr unsafe delete --force --id "$attr_b_id" + run_otdfctl_attr unsafe delete --force --id "$attr_c_id" +} + @test "List - comprehensive pagination tests" { # create 10 random attributes so we have confidence there are >= 10 attribute definitions for i in {1..10}; do diff --git a/otdfctl/e2e/kas-keys.bats b/otdfctl/e2e/kas-keys.bats index 3f7c64b052..ddb2b6d7f3 100644 --- a/otdfctl/e2e/kas-keys.bats +++ b/otdfctl/e2e/kas-keys.bats @@ -618,6 +618,52 @@ format_kas_name_as_uri() { assert_not_equal "$(echo "$output" | jq -r --arg id "${key2_system_id}" '.kas_keys[] | select(.key.id == $id) | .key.metadata.updated_at')" "null" } +@test "kas-keys: list keys supports sort and order flags" { + sort_prefix="sort-key-$BATS_TEST_NUMBER-$RANDOM" + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${sort_prefix}-alpha" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + key_a_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${sort_prefix}-bravo" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + key_b_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${sort_prefix}-charlie" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64_RSA}" --json + assert_success + key_c_id=$(echo "$output" | jq -r .key.id) + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort key_id --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.kas_keys[] | select(.key.key_id | startswith($prefix)) | .key.id] | join(",")')" "$key_c_id,$key_b_id,$key_a_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort key_id --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.kas_keys[] | select(.key.key_id | startswith($prefix)) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$key_a_id" --arg b "$key_b_id" --arg c "$key_c_id" '[.kas_keys[] | select(.key.id == $a or .key.id == $b or .key.id == $c) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" + + run_otdfctl_key update --id "$key_a_id" --label sort=a --json + assert_success + run_otdfctl_key update --id "$key_b_id" --label sort=b --json + assert_success + run_otdfctl_key update --id "$key_c_id" --label sort=c --json + assert_success + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$key_a_id" --arg b "$key_b_id" --arg c "$key_c_id" '[.kas_keys[] | select(.key.id == $a or .key.id == $b or .key.id == $c) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --sort key_id --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.kas_keys[] | select(.key.key_id | startswith($prefix)) | .key.id] | join(",")')" "$key_c_id,$key_b_id,$key_a_id" + + run_otdfctl_key list --kas "${KAS_REGISTRY_ID}" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$key_a_id" --arg b "$key_b_id" --arg c "$key_c_id" '[.kas_keys[] | select(.key.id == $a or .key.id == $b or .key.id == $c) | .key.id] | join(",")')" "$key_a_id,$key_b_id,$key_c_id" +} + @test "kas-keys: list keys (pagination with limit and offset)" { KAS_NAME_LIST=$(generate_kas_name) KAS_URI_LIST=$(format_kas_name_as_uri "${KAS_NAME_LIST}") diff --git a/otdfctl/e2e/kas-registry.bats b/otdfctl/e2e/kas-registry.bats index acc4f7f229..f97eda9de6 100755 --- a/otdfctl/e2e/kas-registry.bats +++ b/otdfctl/e2e/kas-registry.bats @@ -21,8 +21,14 @@ setup() { } teardown() { + if [[ -z "${CREATED:-}" ]]; then + return + fi + ID=$(echo "$CREATED" | jq -r '.id') - run_otdfctl_kasr delete --id "$ID" --force + if [[ -n "$ID" ]]; then + run_otdfctl_kasr delete --id "$ID" --force + fi } @test "create KAS registration with invalid URI - fails" { @@ -156,3 +162,53 @@ teardown() { total=$(echo "$output" | jq -r ".pagination.total") [[ $total -ge 1 ]] } + +@test "list registered KASes supports sort and order flags" { + export CREATED="" + sort_prefix="sort-kas-$BATS_TEST_NUMBER-$RANDOM" + kas_a=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$sort_prefix-alpha" --uri "https://$sort_prefix-alpha.example.com" --json) + kas_b=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$sort_prefix-bravo" --uri "https://$sort_prefix-bravo.example.com" --json) + kas_c=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --name "$sort_prefix-charlie" --uri "https://$sort_prefix-charlie.example.com" --json) + kas_a_id=$(echo "$kas_a" | jq -r '.id') + kas_b_id=$(echo "$kas_b" | jq -r '.id') + kas_c_id=$(echo "$kas_c" | jq -r '.id') + + run_otdfctl_kasr list --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.key_access_servers[] | select((.name // "") | startswith($prefix)) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr list --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.key_access_servers[] | select((.name // "") | startswith($prefix)) | .id] | join(",")')" "$kas_c_id,$kas_b_id,$kas_a_id" + + run_otdfctl_kasr list --sort uri --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "https://$sort_prefix" '[.key_access_servers[] | select(.uri | startswith($prefix)) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr list --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$kas_a_id" --arg b "$kas_b_id" --arg c "$kas_c_id" '[.key_access_servers[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr update --id "$kas_a_id" --label sort=a --json + assert_success + run_otdfctl_kasr update --id "$kas_b_id" --label sort=b --json + assert_success + run_otdfctl_kasr update --id "$kas_c_id" --label sort=c --json + assert_success + + run_otdfctl_kasr list --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$kas_a_id" --arg b "$kas_b_id" --arg c "$kas_c_id" '[.key_access_servers[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr list --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.key_access_servers[] | select((.name // "") | startswith($prefix)) | .id] | join(",")')" "$kas_c_id,$kas_b_id,$kas_a_id" + + run_otdfctl_kasr list --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$kas_a_id" --arg b "$kas_b_id" --arg c "$kas_c_id" '[.key_access_servers[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$kas_a_id,$kas_b_id,$kas_c_id" + + run_otdfctl_kasr delete --id "$kas_a_id" --force + run_otdfctl_kasr delete --id "$kas_b_id" --force + run_otdfctl_kasr delete --id "$kas_c_id" --force +} diff --git a/otdfctl/e2e/namespaces.bats b/otdfctl/e2e/namespaces.bats index 7ddf86d639..2e0c30e63c 100755 --- a/otdfctl/e2e/namespaces.bats +++ b/otdfctl/e2e/namespaces.bats @@ -122,6 +122,55 @@ teardown_file() { assert_line --regexp "Current Offset.*0" } +@test "List namespaces supports sort and order flags" { + sort_prefix="sort-ns-$BATS_TEST_NUMBER-$RANDOM" + ns_a_name="$sort_prefix-alpha.test" + ns_b_name="$sort_prefix-bravo.test" + ns_c_name="$sort_prefix-charlie.test" + ns_a_id=$(./otdfctl $HOST $WITH_CREDS policy namespaces create --name "$ns_a_name" --json | jq -r '.id') + ns_b_id=$(./otdfctl $HOST $WITH_CREDS policy namespaces create --name "$ns_b_name" --json | jq -r '.id') + ns_c_id=$(./otdfctl $HOST $WITH_CREDS policy namespaces create --name "$ns_c_name" --json | jq -r '.id') + + run_otdfctl_nsd list --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.namespaces[] | select(.name | startswith($prefix)) | .name] | join(",")')" "$ns_a_name,$ns_b_name,$ns_c_name" + + run_otdfctl_nsd list --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.namespaces[] | select(.name | startswith($prefix)) | .name] | join(",")')" "$ns_c_name,$ns_b_name,$ns_a_name" + + run_otdfctl_nsd list --sort fqn --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "https://$sort_prefix" '[.namespaces[] | select(.fqn | startswith($prefix)) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd list --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$ns_a_id" --arg b "$ns_b_id" --arg c "$ns_c_id" '[.namespaces[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd update --id "$ns_a_id" --label sort=a --json + assert_success + run_otdfctl_nsd update --id "$ns_b_id" --label sort=b --json + assert_success + run_otdfctl_nsd update --id "$ns_c_id" --label sort=c --json + assert_success + + run_otdfctl_nsd list --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$ns_a_id" --arg b "$ns_b_id" --arg c "$ns_c_id" '[.namespaces[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd list --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.namespaces[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$ns_c_id,$ns_b_id,$ns_a_id" + + run_otdfctl_nsd list --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$ns_a_id" --arg b "$ns_b_id" --arg c "$ns_c_id" '[.namespaces[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$ns_a_id,$ns_b_id,$ns_c_id" + + run_otdfctl_nsd unsafe delete --id "$ns_a_id" --force + run_otdfctl_nsd unsafe delete --id "$ns_b_id" --force + run_otdfctl_nsd unsafe delete --id "$ns_c_id" --force +} + @test "Update namespace - Safe" { # extend labels run_otdfctl_ns update "$NS_ID_FLAG" -l key=value --label test=true diff --git a/otdfctl/e2e/obligations.bats b/otdfctl/e2e/obligations.bats index 5a6efac95f..0d164618bc 100644 --- a/otdfctl/e2e/obligations.bats +++ b/otdfctl/e2e/obligations.bats @@ -427,6 +427,55 @@ teardown_file() { run_otdfctl_obl delete --id $obl2_id --force } +@test "List obligations supports sort and order flags" { + sort_prefix="sort_obl_${BATS_TEST_NUMBER}_$RANDOM" + run_otdfctl_obl create --name "${sort_prefix}_alpha" --namespace "$NS_ID" --json + obl_a_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl create --name "${sort_prefix}_bravo" --namespace "$NS_ID" --json + obl_b_id="$(echo "$output" | jq -r '.id')" + run_otdfctl_obl create --name "${sort_prefix}_charlie" --namespace "$NS_ID" --json + obl_c_id="$(echo "$output" | jq -r '.id')" + + run_otdfctl_obl list --namespace "$NS_ID" --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.obligations[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.obligations[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$obl_c_id,$obl_b_id,$obl_a_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort fqn --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "https://$NS_NAME/obl/$sort_prefix" '[.obligations[] | select(.fqn | startswith($prefix)) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$obl_a_id" --arg b "$obl_b_id" --arg c "$obl_c_id" '[.obligations[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl update --id "$obl_a_id" --label sort=a --json + assert_success + run_otdfctl_obl update --id "$obl_b_id" --label sort=b --json + assert_success + run_otdfctl_obl update --id "$obl_c_id" --label sort=c --json + assert_success + + run_otdfctl_obl list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$obl_a_id" --arg b "$obl_b_id" --arg c "$obl_c_id" '[.obligations[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl list --namespace "$NS_ID" --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.obligations[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$obl_c_id,$obl_b_id,$obl_a_id" + + run_otdfctl_obl list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$obl_a_id" --arg b "$obl_b_id" --arg c "$obl_c_id" '[.obligations[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$obl_a_id,$obl_b_id,$obl_c_id" + + run_otdfctl_obl delete --id "$obl_a_id" --force + run_otdfctl_obl delete --id "$obl_b_id" --force + run_otdfctl_obl delete --id "$obl_c_id" --force +} + @test "Update obligation" { # setup an obligation to update run_otdfctl_obl create --name test_update_obl --namespace "$NS_ID" --json diff --git a/otdfctl/e2e/registered-resources.bats b/otdfctl/e2e/registered-resources.bats index e42e86d196..4bc095bed6 100644 --- a/otdfctl/e2e/registered-resources.bats +++ b/otdfctl/e2e/registered-resources.bats @@ -191,6 +191,51 @@ teardown_file() { run_otdfctl_reg_res delete --id $reg_res2_id --force } +@test "List registered resources supports sort and order flags" { + sort_prefix="sort_rr_${BATS_TEST_NUMBER}_$RANDOM" + run_otdfctl_reg_res create --name "${sort_prefix}_alpha" --namespace "$NS_ID" --json + rr_a_id=$(echo "$output" | jq -r '.id') + run_otdfctl_reg_res create --name "${sort_prefix}_bravo" --namespace "$NS_ID" --json + rr_b_id=$(echo "$output" | jq -r '.id') + run_otdfctl_reg_res create --name "${sort_prefix}_charlie" --namespace "$NS_ID" --json + rr_c_id=$(echo "$output" | jq -r '.id') + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort name --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.resources[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort name --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.resources[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$rr_c_id,$rr_b_id,$rr_a_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$rr_a_id" --arg b "$rr_b_id" --arg c "$rr_c_id" '[.resources[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res update --id "$rr_a_id" --label sort=a --json + assert_success + run_otdfctl_reg_res update --id "$rr_b_id" --label sort=b --json + assert_success + run_otdfctl_reg_res update --id "$rr_c_id" --label sort=c --json + assert_success + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$rr_a_id" --arg b "$rr_b_id" --arg c "$rr_c_id" '[.resources[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --sort name --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg prefix "$sort_prefix" '[.resources[] | select(.name | startswith($prefix)) | .id] | join(",")')" "$rr_c_id,$rr_b_id,$rr_a_id" + + run_otdfctl_reg_res list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$rr_a_id" --arg b "$rr_b_id" --arg c "$rr_c_id" '[.resources[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$rr_a_id,$rr_b_id,$rr_c_id" + + run_otdfctl_reg_res delete --id "$rr_a_id" --force + run_otdfctl_reg_res delete --id "$rr_b_id" --force + run_otdfctl_reg_res delete --id "$rr_c_id" --force +} + @test "Update registered resource" { # setup a resource to update run_otdfctl_reg_res create --name test_update_rr --namespace "$NS_ID" diff --git a/otdfctl/e2e/subject-condition-sets.bats b/otdfctl/e2e/subject-condition-sets.bats index 1b796b916d..1c7b9aef20 100755 --- a/otdfctl/e2e/subject-condition-sets.bats +++ b/otdfctl/e2e/subject-condition-sets.bats @@ -138,6 +138,43 @@ teardown_file() { assert_output --partial "$CREATED_ID" } +@test "List SCS supports sort and order flags" { + scs_a_id=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_1" --namespace "$NS_ID" --json | jq -r '.id') + scs_b_id=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" --namespace "$NS_ID" --json | jq -r '.id') + scs_c_id=$(./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_3" --namespace "$NS_ID" --json | jq -r '.id') + + run_otdfctl_scs list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_a_id,$scs_b_id,$scs_c_id" + + run_otdfctl_scs list --namespace "$NS_ID" --sort created_at --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_c_id,$scs_b_id,$scs_a_id" + + run ./otdfctl $HOST $WITH_CREDS policy scs update --id "$scs_a_id" --subject-sets "$SCS_1" --label sort=a --json + assert_success + run ./otdfctl $HOST $WITH_CREDS policy scs update --id "$scs_b_id" --subject-sets "$SCS_2" --label sort=b --json + assert_success + run ./otdfctl $HOST $WITH_CREDS policy scs update --id "$scs_c_id" --subject-sets "$SCS_3" --label sort=c --json + assert_success + + run_otdfctl_scs list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_a_id,$scs_b_id,$scs_c_id" + + run_otdfctl_scs list --namespace "$NS_ID" --sort created_at --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_c_id,$scs_b_id,$scs_a_id" + + run_otdfctl_scs list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$scs_a_id" --arg b "$scs_b_id" --arg c "$scs_c_id" '[.subject_condition_sets[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$scs_a_id,$scs_b_id,$scs_c_id" + + run_delete_scs "$scs_a_id" + run_delete_scs "$scs_b_id" + run_delete_scs "$scs_c_id" +} + @test "Create a SCS with namespace id" { run ./otdfctl $HOST $WITH_CREDS policy scs create --subject-sets "$SCS_2" --namespace "$NS_ID" assert_output --partial "Id" diff --git a/otdfctl/e2e/subject-mapping.bats b/otdfctl/e2e/subject-mapping.bats index 5384d6f176..a92107fcfe 100755 --- a/otdfctl/e2e/subject-mapping.bats +++ b/otdfctl/e2e/subject-mapping.bats @@ -170,6 +170,47 @@ teardown_file() { [[ "$total" -ge 1 ]] } +@test "List subject mappings supports sort and order flags" { + sort_attr=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name "sort_sm_${BATS_TEST_NUMBER}_$RANDOM" --rule ANY_OF -v "sort_sm_a" -v "sort_sm_b" -v "sort_sm_c" --json) + sort_val_a_id=$(echo "$sort_attr" | jq -r '.values[0].id') + sort_val_b_id=$(echo "$sort_attr" | jq -r '.values[1].id') + sort_val_c_id=$(echo "$sort_attr" | jq -r '.values[2].id') + sm_a_id=$(./otdfctl $HOST $WITH_CREDS policy sm create --namespace "$NS_ID" -a "$sort_val_a_id" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json | jq -r '.id') + sm_b_id=$(./otdfctl $HOST $WITH_CREDS policy sm create --namespace "$NS_ID" -a "$sort_val_b_id" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json | jq -r '.id') + sm_c_id=$(./otdfctl $HOST $WITH_CREDS policy sm create --namespace "$NS_ID" -a "$sort_val_c_id" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_1" --json | jq -r '.id') + + run_otdfctl_sm list --namespace "$NS_ID" --sort created_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_a_id,$sm_b_id,$sm_c_id" + + run_otdfctl_sm list --namespace "$NS_ID" --sort created_at --order desc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_c_id,$sm_b_id,$sm_a_id" + + run_otdfctl_sm update --id "$sm_a_id" --label sort=a --json + assert_success + run_otdfctl_sm update --id "$sm_b_id" --label sort=b --json + assert_success + run_otdfctl_sm update --id "$sm_c_id" --label sort=c --json + assert_success + + run_otdfctl_sm list --namespace "$NS_ID" --sort updated_at --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_a_id,$sm_b_id,$sm_c_id" + + run_otdfctl_sm list --namespace "$NS_ID" --sort created_at --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_c_id,$sm_b_id,$sm_a_id" + + run_otdfctl_sm list --namespace "$NS_ID" --order asc --limit 500 --json + assert_success + assert_equal "$(echo "$output" | jq -r --arg a "$sm_a_id" --arg b "$sm_b_id" --arg c "$sm_c_id" '[.subject_mappings[] | select(.id == $a or .id == $b or .id == $c) | .id] | join(",")')" "$sm_a_id,$sm_b_id,$sm_c_id" + + run_otdfctl_sm delete --id "$sm_a_id" --force + run_otdfctl_sm delete --id "$sm_b_id" --force + run_otdfctl_sm delete --id "$sm_c_id" --force +} + @test "Create subject mapping with namespace ID" { run ./otdfctl $HOST $WITH_CREDS policy subject-mappings create -a "$SM_VAL2_ID" --action "$ACTION_READ_NAME" --subject-condition-set-new "$SCS_2" --namespace "$NS_ID" --json assert_success diff --git a/otdfctl/migrations/namespacedpolicy/planner.go b/otdfctl/migrations/namespacedpolicy/planner.go index b268936922..edbc929394 100644 --- a/otdfctl/migrations/namespacedpolicy/planner.go +++ b/otdfctl/migrations/namespacedpolicy/planner.go @@ -4,6 +4,7 @@ import ( "context" "errors" + "github.com/opentdf/platform/otdfctl/pkg/handlers" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/actions" @@ -19,12 +20,12 @@ var ErrNilPlannerHandler = errors.New("planner handler is required") type PolicyClient interface { ListActions(ctx context.Context, limit, offset int32, namespace string) (*actions.ListActionsResponse, error) - ListSubjectConditionSets(ctx context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectConditionSetsResponse, error) - ListSubjectMappings(ctx context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectMappingsResponse, error) - ListRegisteredResources(ctx context.Context, limit, offset int32, namespace string) (*registeredresources.ListRegisteredResourcesResponse, error) + ListSubjectConditionSets(ctx context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) + ListSubjectMappings(ctx context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) + ListRegisteredResources(ctx context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) ListRegisteredResourceValues(ctx context.Context, resourceID string, limit, offset int32) (*registeredresources.ListRegisteredResourceValuesResponse, error) ListObligationTriggers(ctx context.Context, namespace string, limit, offset int32) (*obligations.ListObligationTriggersResponse, error) - ListNamespaces(ctx context.Context, state common.ActiveStateEnum, limit, offset int32) (*namespaces.ListNamespacesResponse, error) + ListNamespaces(ctx context.Context, state common.ActiveStateEnum, limit, offset int32, sort handlers.SortOption) (*namespaces.ListNamespacesResponse, error) } type Planner struct { diff --git a/otdfctl/migrations/namespacedpolicy/planner_test.go b/otdfctl/migrations/namespacedpolicy/planner_test.go index b6464f3e0d..a86f9bba9c 100644 --- a/otdfctl/migrations/namespacedpolicy/planner_test.go +++ b/otdfctl/migrations/namespacedpolicy/planner_test.go @@ -5,6 +5,7 @@ import ( "errors" "testing" + "github.com/opentdf/platform/otdfctl/pkg/handlers" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/actions" @@ -975,7 +976,7 @@ func (h *plannerTestHandler) ListActions(_ context.Context, limit, offset int32, return &actions.ListActionsResponse{Pagination: emptyPageResponse()}, nil } -func (h *plannerTestHandler) ListSubjectConditionSets(_ context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectConditionSetsResponse, error) { +func (h *plannerTestHandler) ListSubjectConditionSets(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) { h.subjectConditionSetCalls = append(h.subjectConditionSetCalls, namespace) if resp, ok := h.subjectConditionSetsByNamespace[namespace]; ok { return resp, nil @@ -983,7 +984,7 @@ func (h *plannerTestHandler) ListSubjectConditionSets(_ context.Context, limit, return &subjectmapping.ListSubjectConditionSetsResponse{Pagination: emptyPageResponse()}, nil } -func (h *plannerTestHandler) ListSubjectMappings(_ context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectMappingsResponse, error) { +func (h *plannerTestHandler) ListSubjectMappings(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) { h.subjectMappingCalls = append(h.subjectMappingCalls, namespace) if resp, ok := h.subjectMappingsByNamespace[namespace]; ok { return resp, nil @@ -991,7 +992,7 @@ func (h *plannerTestHandler) ListSubjectMappings(_ context.Context, limit, offse return &subjectmapping.ListSubjectMappingsResponse{Pagination: emptyPageResponse()}, nil } -func (h *plannerTestHandler) ListRegisteredResources(_ context.Context, limit, offset int32, namespace string) (*registeredresources.ListRegisteredResourcesResponse, error) { +func (h *plannerTestHandler) ListRegisteredResources(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) { h.registeredResourceCalls = append(h.registeredResourceCalls, namespace) if resp, ok := h.registeredResourcesByNamespace[namespace]; ok { return resp, nil @@ -1028,7 +1029,7 @@ func (h *plannerTestHandler) ListObligationTriggers(_ context.Context, namespace return &obligations.ListObligationTriggersResponse{Pagination: emptyPageResponse()}, nil } -func (h *plannerTestHandler) ListNamespaces(_ context.Context, state common.ActiveStateEnum, limit, offset int32) (*namespaces.ListNamespacesResponse, error) { +func (h *plannerTestHandler) ListNamespaces(_ context.Context, state common.ActiveStateEnum, limit, offset int32, sort handlers.SortOption) (*namespaces.ListNamespacesResponse, error) { if h.namespacesResponse != nil { return h.namespacesResponse, nil } diff --git a/otdfctl/migrations/namespacedpolicy/retrieve.go b/otdfctl/migrations/namespacedpolicy/retrieve.go index 4a89cf46ec..2cb0f2d9dd 100644 --- a/otdfctl/migrations/namespacedpolicy/retrieve.go +++ b/otdfctl/migrations/namespacedpolicy/retrieve.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/opentdf/platform/otdfctl/pkg/handlers" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/actions" @@ -94,7 +95,7 @@ func (r *Retriever) listNamespaces(ctx context.Context) ([]*policy.Namespace, er ) for { - resp, err := r.handler.ListNamespaces(ctx, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE, r.pageSize, offset) + resp, err := r.handler.ListNamespaces(ctx, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ACTIVE, r.pageSize, offset, handlers.SortOption{}) if err != nil { return nil, fmt.Errorf("list namespaces: %w", err) } @@ -182,7 +183,7 @@ func (r *Retriever) retrieveSubjectMappings(ctx context.Context) ([]*policy.Subj ) for { - resp, err := r.handler.ListSubjectMappings(ctx, r.pageSize, offset, "") + resp, err := r.handler.ListSubjectMappings(ctx, r.pageSize, offset, "", handlers.SortOption{}) if err != nil { return nil, fmt.Errorf("list subject mappings: %w", err) } @@ -217,7 +218,7 @@ func (r *Retriever) retrieveSubjectConditionSets(ctx context.Context) ([]*policy var offset int32 for { - resp, err := r.handler.ListSubjectConditionSets(ctx, r.pageSize, offset, "") + resp, err := r.handler.ListSubjectConditionSets(ctx, r.pageSize, offset, "", handlers.SortOption{}) if err != nil { return nil, fmt.Errorf("list subject condition sets: %w", err) } @@ -256,7 +257,7 @@ func (r *Retriever) retrieveRegisteredResources(ctx context.Context) ([]*policy. ) for { - resp, err := r.handler.ListRegisteredResources(ctx, r.pageSize, offset, "") + resp, err := r.handler.ListRegisteredResources(ctx, r.pageSize, offset, "", handlers.SortOption{}) if err != nil { return nil, fmt.Errorf("list registered resources: %w", err) } @@ -458,7 +459,7 @@ func (r *Retriever) listSubjectConditionSetsForNamespaces(ctx context.Context, n for _, namespace := range dedupeTargetNamespaces(namespaces) { var offset int32 for { - resp, err := r.handler.ListSubjectConditionSets(ctx, r.pageSize, offset, namespace.GetId()) + resp, err := r.handler.ListSubjectConditionSets(ctx, r.pageSize, offset, namespace.GetId(), handlers.SortOption{}) if err != nil { return nil, fmt.Errorf("list subject condition sets for namespace %s: %w", namespace.GetId(), err) } @@ -490,7 +491,7 @@ func (r *Retriever) listSubjectMappingsForNamespaces(ctx context.Context, namesp for _, namespace := range dedupeTargetNamespaces(namespaces) { var offset int32 for { - resp, err := r.handler.ListSubjectMappings(ctx, r.pageSize, offset, namespace.GetId()) + resp, err := r.handler.ListSubjectMappings(ctx, r.pageSize, offset, namespace.GetId(), handlers.SortOption{}) if err != nil { return nil, fmt.Errorf("list subject mappings for namespace %s: %w", namespace.GetId(), err) } @@ -522,7 +523,7 @@ func (r *Retriever) listRegisteredResourcesForNamespaces(ctx context.Context, na for _, namespace := range dedupeTargetNamespaces(namespaces) { var offset int32 for { - resp, err := r.handler.ListRegisteredResources(ctx, r.pageSize, offset, namespace.GetId()) + resp, err := r.handler.ListRegisteredResources(ctx, r.pageSize, offset, namespace.GetId(), handlers.SortOption{}) if err != nil { return nil, fmt.Errorf("list registered resources for namespace %s: %w", namespace.GetId(), err) } diff --git a/otdfctl/migrations/namespacedpolicy/retrieve_test.go b/otdfctl/migrations/namespacedpolicy/retrieve_test.go index b150571341..8a04757e9a 100644 --- a/otdfctl/migrations/namespacedpolicy/retrieve_test.go +++ b/otdfctl/migrations/namespacedpolicy/retrieve_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/opentdf/platform/otdfctl/pkg/handlers" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/actions" @@ -415,18 +416,18 @@ func (h *pagedRetrieveTestHandler) ListActions(_ context.Context, limit, offset return &actions.ListActionsResponse{Pagination: emptyPageResponse()}, nil } -func (h *pagedRetrieveTestHandler) ListSubjectConditionSets(_ context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectConditionSetsResponse, error) { +func (h *pagedRetrieveTestHandler) ListSubjectConditionSets(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) { return &subjectmapping.ListSubjectConditionSetsResponse{Pagination: emptyPageResponse()}, nil } -func (h *pagedRetrieveTestHandler) ListSubjectMappings(_ context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectMappingsResponse, error) { +func (h *pagedRetrieveTestHandler) ListSubjectMappings(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) { if resp, ok := h.subjectMappingPages[offset]; ok { return resp, nil } return &subjectmapping.ListSubjectMappingsResponse{Pagination: emptyPageResponse()}, nil } -func (h *pagedRetrieveTestHandler) ListRegisteredResources(_ context.Context, limit, offset int32, namespace string) (*registeredresources.ListRegisteredResourcesResponse, error) { +func (h *pagedRetrieveTestHandler) ListRegisteredResources(_ context.Context, limit, offset int32, namespace string, sort handlers.SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) { if resp, ok := h.registeredResourcePages[offset]; ok { return resp, nil } @@ -463,7 +464,7 @@ func (h *pagedRetrieveTestHandler) ListObligationTriggers(_ context.Context, nam return &obligations.ListObligationTriggersResponse{Pagination: emptyPageResponse()}, nil } -func (h *pagedRetrieveTestHandler) ListNamespaces(_ context.Context, state common.ActiveStateEnum, limit, offset int32) (*namespaces.ListNamespacesResponse, error) { +func (h *pagedRetrieveTestHandler) ListNamespaces(_ context.Context, state common.ActiveStateEnum, limit, offset int32, sort handlers.SortOption) (*namespaces.ListNamespacesResponse, error) { return &namespaces.ListNamespacesResponse{Pagination: emptyPageResponse()}, nil } diff --git a/otdfctl/pkg/handlers/attribute.go b/otdfctl/pkg/handlers/attribute.go index d420cdffe8..dd31f0a2d7 100644 --- a/otdfctl/pkg/handlers/attribute.go +++ b/otdfctl/pkg/handlers/attribute.go @@ -54,14 +54,27 @@ func (h Handler) GetAttribute(ctx context.Context, identifier string) (*policy.A return resp.GetAttribute(), nil } -func (h Handler) ListAttributes(ctx context.Context, state common.ActiveStateEnum, limit, offset int32) (*attributes.ListAttributesResponse, error) { - return h.sdk.Attributes.ListAttributes(ctx, &attributes.ListAttributesRequest{ +func (h Handler) ListAttributes(ctx context.Context, state common.ActiveStateEnum, limit, offset int32, sort SortOption) (*attributes.ListAttributesResponse, error) { + req := &attributes.ListAttributesRequest{ State: state, Pagination: &policy.PageRequest{ Limit: limit, Offset: offset, }, - }) + } + if !sort.IsZero() { + allowedFields := map[string]attributes.SortAttributesType{ + "name": attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_NAME, + "created_at": attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_CREATED_AT, + "updated_at": attributes.SortAttributesType_SORT_ATTRIBUTES_TYPE_UPDATED_AT, + } + field, err := sortField("attributes", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*attributes.AttributesSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.Attributes.ListAttributes(ctx, req) } // Creates and returns the created attribute diff --git a/otdfctl/pkg/handlers/kas-keys.go b/otdfctl/pkg/handlers/kas-keys.go index d18fb66c99..d664115734 100644 --- a/otdfctl/pkg/handlers/kas-keys.go +++ b/otdfctl/pkg/handlers/kas-keys.go @@ -91,6 +91,7 @@ func (h Handler) ListKasKeys( algorithm policy.Algorithm, identifier KasIdentifier, legacy *bool, + sort SortOption, ) (*kasregistry.ListKeysResponse, error) { req := kasregistry.ListKeysRequest{ Pagination: &policy.PageRequest{ @@ -115,6 +116,18 @@ func (h Handler) ListKasKeys( } } req.Legacy = legacy + if !sort.IsZero() { + allowedFields := map[string]kasregistry.SortKasKeysType{ + "key_id": kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_KEY_ID, + "created_at": kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_CREATED_AT, + "updated_at": kasregistry.SortKasKeysType_SORT_KAS_KEYS_TYPE_UPDATED_AT, + } + field, err := sortField("KAS keys", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*kasregistry.KasKeysSort{{Field: field, Direction: sort.Direction}} + } return h.sdk.KeyAccessServerRegistry.ListKeys(ctx, &req) } diff --git a/otdfctl/pkg/handlers/kas-registry.go b/otdfctl/pkg/handlers/kas-registry.go index eaca184228..a0d52a84dc 100644 --- a/otdfctl/pkg/handlers/kas-registry.go +++ b/otdfctl/pkg/handlers/kas-registry.go @@ -42,13 +42,27 @@ func (h Handler) GetKasRegistryEntry(ctx context.Context, identifer KasIdentifie return resp.GetKeyAccessServer(), nil } -func (h Handler) ListKasRegistryEntries(ctx context.Context, limit, offset int32) (*kasregistry.ListKeyAccessServersResponse, error) { - return h.sdk.KeyAccessServerRegistry.ListKeyAccessServers(ctx, &kasregistry.ListKeyAccessServersRequest{ +func (h Handler) ListKasRegistryEntries(ctx context.Context, limit, offset int32, sort SortOption) (*kasregistry.ListKeyAccessServersResponse, error) { + req := &kasregistry.ListKeyAccessServersRequest{ Pagination: &policy.PageRequest{ Limit: limit, Offset: offset, }, - }) + } + if !sort.IsZero() { + allowedFields := map[string]kasregistry.SortKeyAccessServersType{ + "name": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_NAME, + "uri": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_URI, + "created_at": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_CREATED_AT, + "updated_at": kasregistry.SortKeyAccessServersType_SORT_KEY_ACCESS_SERVERS_TYPE_UPDATED_AT, + } + field, err := sortField("KAS registry entries", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*kasregistry.KeyAccessServersSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.KeyAccessServerRegistry.ListKeyAccessServers(ctx, req) } // Creates the KAS registry and then returns the KAS diff --git a/otdfctl/pkg/handlers/namespaces.go b/otdfctl/pkg/handlers/namespaces.go index 6cc2284cab..b7ebbdb0bd 100644 --- a/otdfctl/pkg/handlers/namespaces.go +++ b/otdfctl/pkg/handlers/namespaces.go @@ -38,14 +38,28 @@ func (h Handler) GetNamespace(ctx context.Context, identifier string) (*policy.N return resp.GetNamespace(), nil } -func (h Handler) ListNamespaces(ctx context.Context, state common.ActiveStateEnum, limit, offset int32) (*namespaces.ListNamespacesResponse, error) { - return h.sdk.Namespaces.ListNamespaces(ctx, &namespaces.ListNamespacesRequest{ +func (h Handler) ListNamespaces(ctx context.Context, state common.ActiveStateEnum, limit, offset int32, sort SortOption) (*namespaces.ListNamespacesResponse, error) { + req := &namespaces.ListNamespacesRequest{ State: state, Pagination: &policy.PageRequest{ Limit: limit, Offset: offset, }, - }) + } + if !sort.IsZero() { + allowedFields := map[string]namespaces.SortNamespacesType{ + "name": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_NAME, + "fqn": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_FQN, + "created_at": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_CREATED_AT, + "updated_at": namespaces.SortNamespacesType_SORT_NAMESPACES_TYPE_UPDATED_AT, + } + field, err := sortField("namespaces", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*namespaces.NamespacesSort{{Field: field, Direction: sort.Direction}} + } + return h.sdk.Namespaces.ListNamespaces(ctx, req) } // Creates and returns the created n diff --git a/otdfctl/pkg/handlers/obligations.go b/otdfctl/pkg/handlers/obligations.go index 59373ae22c..7aae3b761e 100644 --- a/otdfctl/pkg/handlers/obligations.go +++ b/otdfctl/pkg/handlers/obligations.go @@ -63,7 +63,7 @@ func (h Handler) GetObligation(ctx context.Context, id, fqn string) (*policy.Obl return resp.GetObligation(), nil } -func (h Handler) ListObligations(ctx context.Context, limit, offset int32, namespace string) (*obligations.ListObligationsResponse, error) { +func (h Handler) ListObligations(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*obligations.ListObligationsResponse, error) { req := &obligations.ListObligationsRequest{ Pagination: &policy.PageRequest{ Limit: limit, @@ -73,6 +73,19 @@ func (h Handler) ListObligations(ctx context.Context, limit, offset int32, names if namespace != "" { req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) } + if !sort.IsZero() { + allowedFields := map[string]obligations.SortObligationsType{ + "name": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_NAME, + "fqn": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_FQN, + "created_at": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_CREATED_AT, + "updated_at": obligations.SortObligationsType_SORT_OBLIGATIONS_TYPE_UPDATED_AT, + } + field, err := sortField("obligations", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*obligations.ObligationsSort{{Field: field, Direction: sort.Direction}} + } return h.sdk.Obligations.ListObligations(ctx, req) } diff --git a/otdfctl/pkg/handlers/registeredResources.go b/otdfctl/pkg/handlers/registeredResources.go index cbbcc74e01..1672a93adb 100644 --- a/otdfctl/pkg/handlers/registeredResources.go +++ b/otdfctl/pkg/handlers/registeredResources.go @@ -52,7 +52,7 @@ func (h Handler) GetRegisteredResource(ctx context.Context, id, name, namespace return resp.GetResource(), nil } -func (h Handler) ListRegisteredResources(ctx context.Context, limit, offset int32, namespace string) (*registeredresources.ListRegisteredResourcesResponse, error) { +func (h Handler) ListRegisteredResources(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*registeredresources.ListRegisteredResourcesResponse, error) { req := ®isteredresources.ListRegisteredResourcesRequest{ Pagination: &policy.PageRequest{ Limit: limit, @@ -62,6 +62,18 @@ func (h Handler) ListRegisteredResources(ctx context.Context, limit, offset int3 if namespace != "" { req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) } + if !sort.IsZero() { + allowedFields := map[string]registeredresources.SortRegisteredResourcesType{ + "name": registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_NAME, + "created_at": registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_CREATED_AT, + "updated_at": registeredresources.SortRegisteredResourcesType_SORT_REGISTERED_RESOURCES_TYPE_UPDATED_AT, + } + field, err := sortField("registered resources", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*registeredresources.RegisteredResourcesSort{{Field: field, Direction: sort.Direction}} + } return h.sdk.RegisteredResources.ListRegisteredResources(ctx, req) } diff --git a/otdfctl/pkg/handlers/sort.go b/otdfctl/pkg/handlers/sort.go new file mode 100644 index 0000000000..c1bd2cc604 --- /dev/null +++ b/otdfctl/pkg/handlers/sort.go @@ -0,0 +1,87 @@ +package handlers + +import ( + "errors" + "fmt" + "sort" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" +) + +type SortOption struct { + Field string + Direction policy.SortDirection +} + +const ( + sortDirectionAsc = "asc" + sortDirectionDesc = "desc" +) + +var ( + ErrInvalidSortDirection = errors.New("invalid sort direction") + ErrInvalidSortField = errors.New("invalid sort field") +) + +func NewSortOption(field, order string) (SortOption, error) { + field = strings.ToLower(strings.TrimSpace(field)) + direction, err := ParseSortOrder(order) + if err != nil { + return SortOption{}, err + } + + return SortOption{ + Field: field, + Direction: direction, + }, nil +} + +func ParseSortOrder(value string) (policy.SortDirection, error) { + value = strings.TrimSpace(value) + if value == "" { + return policy.SortDirection_SORT_DIRECTION_UNSPECIFIED, nil + } + + switch strings.ToLower(value) { + case sortDirectionAsc: + return policy.SortDirection_SORT_DIRECTION_ASC, nil + case sortDirectionDesc: + return policy.SortDirection_SORT_DIRECTION_DESC, nil + default: + return policy.SortDirection_SORT_DIRECTION_UNSPECIFIED, errors.Join( + ErrInvalidSortDirection, + fmt.Errorf("%q must be asc or desc", value), + ) + } +} + +func (s SortOption) IsZero() bool { + return s.Field == "" && s.Direction == policy.SortDirection_SORT_DIRECTION_UNSPECIFIED +} + +func sortField[T any](resource string, option SortOption, allowed map[string]T) (T, error) { + var zero T + if option.Field == "" { + return zero, nil + } + + field, ok := allowed[option.Field] + if !ok { + return zero, invalidSortFieldError(resource, option.Field, allowed) + } + + return field, nil +} + +func invalidSortFieldError[T any](resource, field string, allowed map[string]T) error { + fields := make([]string, 0, len(allowed)) + for f := range allowed { + fields = append(fields, f) + } + sort.Strings(fields) + return errors.Join( + ErrInvalidSortField, + fmt.Errorf("%q is not a valid sort field for %s; valid fields: %s", field, resource, strings.Join(fields, ", ")), + ) +} diff --git a/otdfctl/pkg/handlers/sort_test.go b/otdfctl/pkg/handlers/sort_test.go new file mode 100644 index 0000000000..ad906f982a --- /dev/null +++ b/otdfctl/pkg/handlers/sort_test.go @@ -0,0 +1,103 @@ +package handlers + +import ( + "testing" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewSortOption(t *testing.T) { + tests := []struct { + name string + field string + order string + expected SortOption + wantError error + }{ + { + name: "empty", + }, + { + name: "field only", + field: "name", + expected: SortOption{ + Field: "name", + Direction: policy.SortDirection_SORT_DIRECTION_UNSPECIFIED, + }, + }, + { + name: "ascending", + field: "created_at", + order: "asc", + expected: SortOption{ + Field: "created_at", + Direction: policy.SortDirection_SORT_DIRECTION_ASC, + }, + }, + { + name: "descending with whitespace", + field: " updated_at ", + order: " DESC ", + expected: SortOption{ + Field: "updated_at", + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + { + name: "direction only", + order: "desc", + expected: SortOption{ + Field: "", + Direction: policy.SortDirection_SORT_DIRECTION_DESC, + }, + }, + { + name: "invalid direction", + field: "name", + order: "up", + wantError: ErrInvalidSortDirection, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := NewSortOption(tt.field, tt.order) + if tt.wantError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.wantError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestSortField(t *testing.T) { + allowed := map[string]int{ + "name": 1, + "created_at": 2, + } + + t.Run("omitted field returns zero value", func(t *testing.T) { + field, err := sortField("test resources", SortOption{}, allowed) + require.NoError(t, err) + assert.Equal(t, 0, field) + }) + + t.Run("known field returns mapped value", func(t *testing.T) { + field, err := sortField("test resources", SortOption{Field: "name"}, allowed) + require.NoError(t, err) + assert.Equal(t, 1, field) + }) + + t.Run("unknown field returns valid fields", func(t *testing.T) { + field, err := sortField("test resources", SortOption{Field: "updated_at"}, allowed) + require.Error(t, err) + assert.Equal(t, 0, field) + require.ErrorIs(t, err, ErrInvalidSortField) + assert.EqualError(t, err, "invalid sort field\n\"updated_at\" is not a valid sort field for test resources; valid fields: created_at, name") + }) +} diff --git a/otdfctl/pkg/handlers/subjectConditionSets.go b/otdfctl/pkg/handlers/subjectConditionSets.go index 76ec21c57c..847d517ab4 100644 --- a/otdfctl/pkg/handlers/subjectConditionSets.go +++ b/otdfctl/pkg/handlers/subjectConditionSets.go @@ -19,7 +19,7 @@ func (h Handler) GetSubjectConditionSet(ctx context.Context, id string) (*policy return resp.GetSubjectConditionSet(), nil } -func (h Handler) ListSubjectConditionSets(ctx context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectConditionSetsResponse, error) { +func (h Handler) ListSubjectConditionSets(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*subjectmapping.ListSubjectConditionSetsResponse, error) { req := &subjectmapping.ListSubjectConditionSetsRequest{ Pagination: &policy.PageRequest{ Limit: limit, @@ -27,6 +27,17 @@ func (h Handler) ListSubjectConditionSets(ctx context.Context, limit, offset int }, } req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + if !sort.IsZero() { + allowedFields := map[string]subjectmapping.SortSubjectConditionSetsType{ + "created_at": subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_CREATED_AT, + "updated_at": subjectmapping.SortSubjectConditionSetsType_SORT_SUBJECT_CONDITION_SETS_TYPE_UPDATED_AT, + } + field, err := sortField("subject condition sets", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*subjectmapping.SubjectConditionSetsSort{{Field: field, Direction: sort.Direction}} + } return h.sdk.SubjectMapping.ListSubjectConditionSets(ctx, req) } diff --git a/otdfctl/pkg/handlers/subjectmappings.go b/otdfctl/pkg/handlers/subjectmappings.go index c93431aa25..6412b7b3df 100644 --- a/otdfctl/pkg/handlers/subjectmappings.go +++ b/otdfctl/pkg/handlers/subjectmappings.go @@ -24,7 +24,7 @@ func (h Handler) GetSubjectMapping(ctx context.Context, id string) (*policy.Subj return resp.GetSubjectMapping(), err } -func (h Handler) ListSubjectMappings(ctx context.Context, limit, offset int32, namespace string) (*subjectmapping.ListSubjectMappingsResponse, error) { +func (h Handler) ListSubjectMappings(ctx context.Context, limit, offset int32, namespace string, sort SortOption) (*subjectmapping.ListSubjectMappingsResponse, error) { req := &subjectmapping.ListSubjectMappingsRequest{ Pagination: &policy.PageRequest{ Limit: limit, @@ -32,6 +32,17 @@ func (h Handler) ListSubjectMappings(ctx context.Context, limit, offset int32, n }, } req.NamespaceId, req.NamespaceFqn = getNamespaceIDAndFQN(namespace) + if !sort.IsZero() { + allowedFields := map[string]subjectmapping.SortSubjectMappingsType{ + "created_at": subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_CREATED_AT, + "updated_at": subjectmapping.SortSubjectMappingsType_SORT_SUBJECT_MAPPINGS_TYPE_UPDATED_AT, + } + field, err := sortField("subject mappings", sort, allowedFields) + if err != nil { + return nil, err + } + req.Sort = []*subjectmapping.SubjectMappingsSort{{Field: field, Direction: sort.Direction}} + } return h.sdk.SubjectMapping.ListSubjectMappings(ctx, req) } diff --git a/otdfctl/tui/attributeList.go b/otdfctl/tui/attributeList.go index cc6676167d..ec3787888e 100644 --- a/otdfctl/tui/attributeList.go +++ b/otdfctl/tui/attributeList.go @@ -40,7 +40,7 @@ func InitAttributeList(ctx context.Context, id string, h handlers.Handler) (tea. limit int32 = 100 offset int32 = 0 ) - res, _ := h.ListAttributes(ctx, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, limit, offset) + res, _ := h.ListAttributes(ctx, common.ActiveStateEnum_ACTIVE_STATE_ENUM_ANY, limit, offset, handlers.SortOption{}) var attrs []list.Item selectIdx := 0 for i, attr := range res.GetAttributes() {