diff --git a/cmd/policy-attributes.go b/cmd/policy-attributes.go index 4bf5fcb5..e86e5ce1 100644 --- a/cmd/policy-attributes.go +++ b/cmd/policy-attributes.go @@ -13,8 +13,8 @@ import ( var ( forceReplaceMetadataLabels bool - values []string - valuesOrder []string + attributeValues []string + attributeValuesOrder []string policy_attributesCmd = man.Docs.GetCommand("policy/attributes") ) @@ -26,11 +26,11 @@ func policy_createAttribute(cmd *cobra.Command, args []string) { name := c.Flags.GetRequiredString("name") rule := c.Flags.GetRequiredString("rule") - values = c.Flags.GetStringSlice("value", values, cli.FlagsStringSliceOptions{}) + attributeValues = c.Flags.GetStringSlice("value", attributeValues, cli.FlagsStringSliceOptions{}) namespace := c.Flags.GetRequiredString("namespace") metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) - attr, err := h.CreateAttribute(cmd.Context(), name, rule, namespace, values, getMetadataMutable(metadataLabels)) + attr, err := h.CreateAttribute(cmd.Context(), name, rule, namespace, attributeValues, getMetadataMutable(metadataLabels)) if err != nil { cli.ExitWithError("Failed to create attribute", err) } @@ -233,7 +233,7 @@ func policy_unsafeUpdateAttribute(cmd *cobra.Command, args []string) { id := c.Flags.GetRequiredID("id") name := c.Flags.GetOptionalString("name") rule := c.Flags.GetOptionalString("rule") - valuesOrder = c.Flags.GetStringSlice("values-order", valuesOrder, cli.FlagsStringSliceOptions{}) + attributeValuesOrder = c.Flags.GetStringSlice("values-order", attributeValuesOrder, cli.FlagsStringSliceOptions{}) a, err := h.GetAttribute(ctx, id) if err != nil { @@ -245,7 +245,7 @@ func policy_unsafeUpdateAttribute(cmd *cobra.Command, args []string) { cli.ConfirmTextInput(cli.ActionUpdateUnsafe, "attribute", cli.InputNameFQN, a.GetFqn()) } - if err := h.UnsafeUpdateAttribute(ctx, id, name, rule, valuesOrder); err != nil { + if err := h.UnsafeUpdateAttribute(ctx, id, name, rule, attributeValuesOrder); err != nil { cli.ExitWithError(fmt.Sprintf("Failed to update attribute (%s)", id), err) } else { var ( @@ -371,7 +371,7 @@ func init() { createDoc.GetDocFlag("rule").Description, ) createDoc.Flags().StringSliceVarP( - &values, + &attributeValues, createDoc.GetDocFlag("value").Name, createDoc.GetDocFlag("value").Shorthand, []string{}, @@ -484,7 +484,7 @@ func init() { unsafeUpdateCmd.GetDocFlag("rule").Description, ) unsafeUpdateCmd.Flags().StringSliceVarP( - &valuesOrder, + &attributeValuesOrder, unsafeUpdateCmd.GetDocFlag("values-order").Name, unsafeUpdateCmd.GetDocFlag("values-order").Shorthand, []string{}, diff --git a/cmd/policy-registeredResources.go b/cmd/policy-registeredResources.go new file mode 100644 index 00000000..bc7e3321 --- /dev/null +++ b/cmd/policy-registeredResources.go @@ -0,0 +1,633 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/evertras/bubble-table/table" + "github.com/google/uuid" + "github.com/opentdf/otdfctl/pkg/cli" + "github.com/opentdf/otdfctl/pkg/man" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/spf13/cobra" +) + +var ( + registeredResourceValues []string + actionAttributeValues []string +) + +const actionAttributeValueArgSplitCount = 2 + +// +// Registered Resources +// + +func policyCreateRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + name := c.Flags.GetRequiredString("name") + registeredResourceValues = c.Flags.GetStringSlice("value", registeredResourceValues, cli.FlagsStringSliceOptions{}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + resource, err := h.CreateRegisteredResource(cmd.Context(), name, registeredResourceValues, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create registered resource", err) + } + + simpleRegResValues := cli.GetSimpleRegisteredResourceValues(resource.GetValues()) + + rows := [][]string{ + {"Id", resource.GetId()}, + {"Name", resource.GetName()}, + {"Values", cli.CommaSeparated(simpleRegResValues)}, + } + + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, resource.GetId(), t, resource) +} + +func policyGetRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + name := c.Flags.GetOptionalString("name") + + if id == "" && name == "" { + cli.ExitWithError("Either 'id' or 'name' must be provided", nil) + } + + resource, err := h.GetRegisteredResource(cmd.Context(), id, name) + if err != nil { + identifier := fmt.Sprintf("id: %s", id) + if id == "" { + identifier = fmt.Sprintf("name: %s", name) + } + errMsg := fmt.Sprintf("Failed to find registered resource (%s)", identifier) + cli.ExitWithError(errMsg, err) + } + + simpleRegResValues := cli.GetSimpleRegisteredResourceValues(resource.GetValues()) + + rows := [][]string{ + {"Id", resource.GetId()}, + {"Name", resource.GetName()}, + {"Values", cli.CommaSeparated(simpleRegResValues)}, + } + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, resource.GetId(), t, resource) +} + +func policyListRegisteredResources(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + resources, page, err := h.ListRegisteredResources(cmd.Context(), limit, offset) + if err != nil { + cli.ExitWithError("Failed to list registered resources", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("name", "Name", cli.FlexColumnWidthFour), + table.NewFlexColumn("values", "Values", cli.FlexColumnWidthTwo), + // todo: do we need to show metadata labels and created/updated at? + ) + rows := []table.Row{} + for _, r := range resources { + simpleRegResValues := cli.GetSimpleRegisteredResourceValues(r.GetValues()) + rows = append(rows, table.NewRow(table.RowData{ + "id": r.GetId(), + "name": r.GetName(), + "values": cli.CommaSeparated(simpleRegResValues), + // todo: do we need to show metadata labels and created/updated at? + })) + } + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, page) + HandleSuccess(cmd, "", t, resources) +} + +func policyUpdateRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + name := c.Flags.GetOptionalString("name") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + updated, err := h.UpdateRegisteredResource( + cmd.Context(), + id, + name, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update registered resource", err) + } + + rows := [][]string{ + {"Id", id}, + {"Name", updated.GetName()}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, id, t, updated) +} + +func policyDeleteRegisteredResource(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetRequiredBool("force") + ctx := cmd.Context() + + resource, err := h.GetRegisteredResource(ctx, id, "") + if err != nil { + errMsg := fmt.Sprintf("Failed to find registered resource (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDelete, "registered resource", id, force) + + err = h.DeleteRegisteredResource(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete registered resource (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", id}, + {"Name", resource.GetName()}, + } + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, id, t, resource) +} + +// +// Registered Resource Values +// + +func policyCreateRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + resource := c.Flags.GetRequiredString("resource") + value := c.Flags.GetRequiredString("value") + actionAttributeValues = c.Flags.GetStringSlice("action-attribute-value", actionAttributeValues, cli.FlagsStringSliceOptions{Min: 0}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + var resourceID string + if uuid.Validate(resource) == nil { + resourceID = resource + } else { + resourceByName, err := h.GetRegisteredResource(ctx, "", resource) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to find registered resource (name: %s)", resource), err) + } + resourceID = resourceByName.GetId() + } + + parsedActionAttributeValues := parseActionAttributeValueArgs(actionAttributeValues) + + resourceValue, err := h.CreateRegisteredResourceValue(ctx, resourceID, value, parsedActionAttributeValues, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create registered resource value", err) + } + + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(resourceValue.GetActionAttributeValues()) + + rows := [][]string{ + {"Id", resourceValue.GetId()}, + {"Value", resourceValue.GetValue()}, + {"Action Attribute Values", cli.CommaSeparated(simpleActionAttributeValues)}, + } + if mdRows := getMetadataRows(resourceValue.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, resourceValue.GetId(), t, resourceValue) +} + +func policyGetRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetOptionalID("id") + fqn := c.Flags.GetOptionalString("fqn") + + if id == "" && fqn == "" { + cli.ExitWithError("Either 'id' or 'fqn' must be provided", nil) + } + + value, err := h.GetRegisteredResourceValue(cmd.Context(), id, fqn) + if err != nil { + identifier := fmt.Sprintf("id: %s", id) + if id == "" { + identifier = fmt.Sprintf("fqn: %s", fqn) + } + errMsg := fmt.Sprintf("Failed to find registered resource value (%s)", identifier) + cli.ExitWithError(errMsg, err) + } + + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(value.GetActionAttributeValues()) + + rows := [][]string{ + {"Id", value.GetId()}, + {"Value", value.GetValue()}, + {"Action Attribute Values", cli.CommaSeparated(simpleActionAttributeValues)}, + } + if mdRows := getMetadataRows(value.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, value.GetId(), t, value) +} + +func policyListRegisteredResourceValues(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + ctx := cmd.Context() + resource := c.Flags.GetRequiredString("resource") + limit := c.Flags.GetRequiredInt32("limit") + offset := c.Flags.GetRequiredInt32("offset") + + var resourceID string + if uuid.Validate(resource) == nil { + resourceID = resource + } else { + resourceByName, err := h.GetRegisteredResource(ctx, "", resource) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to find registered resource (name: %s)", resource), err) + } + resourceID = resourceByName.GetId() + } + + values, page, err := h.ListRegisteredResourceValues(ctx, resourceID, limit, offset) + if err != nil { + cli.ExitWithError("Failed to list registered resource values", err) + } + + t := cli.NewTable( + cli.NewUUIDColumn(), + table.NewFlexColumn("value", "Value", cli.FlexColumnWidthFour), + table.NewFlexColumn("action-attribute-values", "Action Attribute Values", cli.FlexColumnWidthFour), + ) + rows := []table.Row{} + for _, v := range values { + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(v.GetActionAttributeValues()) + + rows = append(rows, table.NewRow(table.RowData{ + "id": v.GetId(), + "value": v.GetValue(), + "action-attribute-values": cli.CommaSeparated(simpleActionAttributeValues), + })) + } + list := append([]*policy.RegisteredResourceValue{}, values...) + + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, page) + HandleSuccess(cmd, "", t, list) +} + +func policyUpdateRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + value := c.Flags.GetOptionalString("value") + actionAttributeValues = c.Flags.GetStringSlice("action-attribute-value", actionAttributeValues, cli.FlagsStringSliceOptions{Min: 0}) + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + parsedActionAttributeValues := parseActionAttributeValueArgs(actionAttributeValues) + + updated, err := h.UpdateRegisteredResourceValue( + cmd.Context(), + id, + value, + parsedActionAttributeValues, + getMetadataMutable(metadataLabels), + getMetadataUpdateBehavior(), + ) + if err != nil { + cli.ExitWithError("Failed to update registered resource value", err) + } + + simpleActionAttributeValues := cli.GetSimpleRegisteredResourceActionAttributeValues(updated.GetActionAttributeValues()) + + rows := [][]string{ + {"Id", id}, + {"Value", updated.GetValue()}, + {"Action Attribute Values", cli.CommaSeparated(simpleActionAttributeValues)}, + } + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, id, t, updated) +} + +func policyDeleteRegisteredResourceValue(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + force := c.Flags.GetOptionalBool("force") + ctx := cmd.Context() + + resource, err := h.GetRegisteredResourceValue(ctx, id, "") + if err != nil { + errMsg := fmt.Sprintf("Failed to find registered resource value (%s)", id) + cli.ExitWithError(errMsg, err) + } + + cli.ConfirmAction(cli.ActionDelete, "registered resource value", id, force) + + err = h.DeleteRegisteredResourceValue(ctx, id) + if err != nil { + errMsg := fmt.Sprintf("Failed to delete registered resource value (%s)", id) + cli.ExitWithError(errMsg, err) + } + + rows := [][]string{ + {"Id", id}, + {"Value", resource.GetValue()}, + } + if mdRows := getMetadataRows(resource.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + HandleSuccess(cmd, id, t, resource) +} + +func parseActionAttributeValueArgs(args []string) []*registeredresources.ActionAttributeValue { + parsed := make([]*registeredresources.ActionAttributeValue, len(args)) + + for i, a := range args { + // split on semicolon + split := strings.Split(a, ";") + if len(split) != actionAttributeValueArgSplitCount { + cli.ExitWithError("Invalid action attribute value arg format", nil) + } + + actionIdentifier := split[0] + attrValIdentifier := split[1] + + newActionAttrVal := ®isteredresources.ActionAttributeValue{} + + if uuid.Validate(actionIdentifier) == nil { + newActionAttrVal.ActionIdentifier = ®isteredresources.ActionAttributeValue_ActionId{ + ActionId: actionIdentifier, + } + } else { + newActionAttrVal.ActionIdentifier = ®isteredresources.ActionAttributeValue_ActionName{ + ActionName: actionIdentifier, + } + } + + if uuid.Validate(attrValIdentifier) == nil { + newActionAttrVal.AttributeValueIdentifier = ®isteredresources.ActionAttributeValue_AttributeValueId{ + AttributeValueId: attrValIdentifier, + } + } else { + newActionAttrVal.AttributeValueIdentifier = ®isteredresources.ActionAttributeValue_AttributeValueFqn{ + AttributeValueFqn: attrValIdentifier, + } + } + + parsed[i] = newActionAttrVal + } + + return parsed +} + +func init() { + // Registered Resources commands + + getDoc := man.Docs.GetCommand("policy/registered-resources/get", + man.WithRun(policyGetRegisteredResource), + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + getDoc.Flags().StringP( + getDoc.GetDocFlag("name").Name, + getDoc.GetDocFlag("name").Shorthand, + getDoc.GetDocFlag("name").Default, + getDoc.GetDocFlag("name").Description, + ) + + listDoc := man.Docs.GetCommand("policy/registered-resources/list", + man.WithRun(policyListRegisteredResources), + ) + injectListPaginationFlags(listDoc) + + createDoc := man.Docs.GetCommand("policy/registered-resources/create", + man.WithRun(policyCreateRegisteredResource), + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("name").Name, + createDoc.GetDocFlag("name").Shorthand, + createDoc.GetDocFlag("name").Default, + createDoc.GetDocFlag("name").Description, + ) + createDoc.Flags().StringSliceVarP( + ®isteredResourceValues, + createDoc.GetDocFlag("value").Name, + createDoc.GetDocFlag("value").Shorthand, + []string{}, + createDoc.GetDocFlag("value").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/registered-resources/update", + man.WithRun(policyUpdateRegisteredResource), + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("name").Name, + updateDoc.GetDocFlag("name").Shorthand, + updateDoc.GetDocFlag("name").Default, + updateDoc.GetDocFlag("name").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + deleteDoc := man.Docs.GetCommand("policy/registered-resources/delete", + man.WithRun(policyDeleteRegisteredResource), + ) + deleteDoc.Flags().StringP( + deleteDoc.GetDocFlag("id").Name, + deleteDoc.GetDocFlag("id").Shorthand, + deleteDoc.GetDocFlag("id").Default, + deleteDoc.GetDocFlag("id").Description, + ) + deleteDoc.Flags().Bool( + deleteDoc.GetDocFlag("force").Name, + false, + deleteDoc.GetDocFlag("force").Description, + ) + + // Registered Resource Values commands + + getValueDoc := man.Docs.GetCommand("policy/registered-resources/values/get", + man.WithRun(policyGetRegisteredResourceValue), + ) + getValueDoc.Flags().StringP( + getValueDoc.GetDocFlag("id").Name, + getValueDoc.GetDocFlag("id").Shorthand, + getValueDoc.GetDocFlag("id").Default, + getValueDoc.GetDocFlag("id").Description, + ) + getValueDoc.Flags().StringP( + getValueDoc.GetDocFlag("fqn").Name, + getValueDoc.GetDocFlag("fqn").Shorthand, + getValueDoc.GetDocFlag("fqn").Default, + getValueDoc.GetDocFlag("fqn").Description, + ) + + listValuesDoc := man.Docs.GetCommand("policy/registered-resources/values/list", + man.WithRun(policyListRegisteredResourceValues), + ) + listValuesDoc.Flags().StringP( + listValuesDoc.GetDocFlag("resource").Name, + listValuesDoc.GetDocFlag("resource").Shorthand, + listValuesDoc.GetDocFlag("resource").Default, + listValuesDoc.GetDocFlag("resource").Description, + ) + injectListPaginationFlags(listValuesDoc) + + createValueDoc := man.Docs.GetCommand("policy/registered-resources/values/create", + man.WithRun(policyCreateRegisteredResourceValue), + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("resource").Name, + createValueDoc.GetDocFlag("resource").Shorthand, + createValueDoc.GetDocFlag("resource").Default, + createValueDoc.GetDocFlag("resource").Description, + ) + createValueDoc.Flags().StringP( + createValueDoc.GetDocFlag("value").Name, + createValueDoc.GetDocFlag("value").Shorthand, + createValueDoc.GetDocFlag("value").Default, + createValueDoc.GetDocFlag("value").Description, + ) + createValueDoc.Flags().StringSliceVarP( + &actionAttributeValues, + createValueDoc.GetDocFlag("action-attribute-value").Name, + createValueDoc.GetDocFlag("action-attribute-value").Shorthand, + []string{}, + createValueDoc.GetDocFlag("action-attribute-value").Description, + ) + injectLabelFlags(&createValueDoc.Command, false) + + updateValueDoc := man.Docs.GetCommand("policy/registered-resources/values/update", + man.WithRun(policyUpdateRegisteredResourceValue), + ) + updateValueDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + updateValueDoc.Flags().StringP( + updateValueDoc.GetDocFlag("value").Name, + updateValueDoc.GetDocFlag("value").Shorthand, + updateValueDoc.GetDocFlag("value").Default, + updateValueDoc.GetDocFlag("value").Description, + ) + updateValueDoc.Flags().StringSliceVarP( + &actionAttributeValues, + updateValueDoc.GetDocFlag("action-attribute-value").Name, + updateValueDoc.GetDocFlag("action-attribute-value").Shorthand, + []string{}, + updateValueDoc.GetDocFlag("action-attribute-value").Description, + ) + injectLabelFlags(&updateValueDoc.Command, true) + + deleteValueDoc := man.Docs.GetCommand("policy/registered-resources/values/delete", + man.WithRun(policyDeleteRegisteredResourceValue), + ) + deleteValueDoc.Flags().StringP( + deleteValueDoc.GetDocFlag("id").Name, + deleteValueDoc.GetDocFlag("id").Shorthand, + deleteValueDoc.GetDocFlag("id").Default, + deleteValueDoc.GetDocFlag("id").Description, + ) + deleteValueDoc.Flags().Bool( + deleteValueDoc.GetDocFlag("force").Name, + false, + deleteValueDoc.GetDocFlag("force").Description, + ) + + // Add commands to the policy command + + policyRegisteredResourcesDoc := man.Docs.GetCommand("policy/registered-resources", + man.WithSubcommands( + getDoc, + listDoc, + createDoc, + updateDoc, + deleteDoc, + ), + ) + + policyRegisteredResourceValuesDoc := man.Docs.GetCommand("policy/registered-resources/values", + man.WithSubcommands( + getValueDoc, + listValuesDoc, + createValueDoc, + updateValueDoc, + deleteValueDoc, + ), + ) + + policyRegisteredResourcesDoc.AddCommand(&policyRegisteredResourceValuesDoc.Command) + policyCmd.AddCommand(&policyRegisteredResourcesDoc.Command) +} diff --git a/docs/man/policy/_index.md b/docs/man/policy/_index.md index 1b849365..a8c76f77 100644 --- a/docs/man/policy/_index.md +++ b/docs/man/policy/_index.md @@ -14,5 +14,5 @@ command: Policy is a set of rules that are enforced by the platform. Specific to the the data-centric security, policy revolves around data attributes (referred to as attributes). Within the context -of attributes are namespaces, values, subject-mappings, resource-mappings, key-access-server grants, +of attributes are namespaces, values, subject-mappings, resource-mappings, registered-resources, key-access-server grants, and other key elements. diff --git a/docs/man/policy/registered-resources/_index.md b/docs/man/policy/registered-resources/_index.md new file mode 100644 index 00000000..19977d76 --- /dev/null +++ b/docs/man/policy/registered-resources/_index.md @@ -0,0 +1,10 @@ +--- +title: Manage Registered Resources +command: + name: registered-resources + aliases: + - reg-res + hidden: true +--- + +Registered Resources are "non-data" resources (i.e. not a TDF data object) that are registered with the platform policy and may serve as the "Entity" or "Resource" in a decision request. diff --git a/docs/man/policy/registered-resources/create.md b/docs/man/policy/registered-resources/create.md new file mode 100644 index 00000000..62641bcc --- /dev/null +++ b/docs/man/policy/registered-resources/create.md @@ -0,0 +1,35 @@ +--- +title: Create a Registered Resource +command: + name: create + aliases: + - c + - add + - new + flags: + - name: name + shorthand: n + description: Name of the registered resource (must be unique within Policy) + required: true + - name: value + shorthand: v + description: Value of the registered resource (i.e. 'value1', must be unique within the Registered Resource) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add a registered resource to the platform Policy. + +A registered resource `name` is normalized to lower case and may contain hyphens or dashes between other alphanumeric characters. + +For more information, see the `registered-resources` subcommand. + +## Examples + +Create a registered resource named 'my_resource' with value 'my_value': + +```shell +otdfctl policy registered-resources create --name my_resource --value my_value +``` diff --git a/docs/man/policy/registered-resources/delete.md b/docs/man/policy/registered-resources/delete.md new file mode 100644 index 00000000..c6413b73 --- /dev/null +++ b/docs/man/policy/registered-resources/delete.md @@ -0,0 +1,24 @@ +--- +title: Delete a Registered Resource +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the registered resource + required: true + - name: force + description: Force deletion without interactive confirmation +--- + +Removes a Registered Resource from platform Policy. + +Registered resource deletion cascades to the associated Registered Resource Values and Action Attribute Values. + +For more information about Registered Resources, see the manual for the `registered-resources` subcommand. + +## Example + +```shell +otdfctl policy registered-resources delete --id 217b300a-47f9-4bee-be8c-d38c880053f7 +``` diff --git a/docs/man/policy/registered-resources/get.md b/docs/man/policy/registered-resources/get.md new file mode 100644 index 00000000..05792722 --- /dev/null +++ b/docs/man/policy/registered-resources/get.md @@ -0,0 +1,34 @@ +--- +title: Get a Registered Resource +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the registered resource + - name: name + shorthand: n + description: Name of the registered resource +--- + +Retrieve a registered resource along with its metadata and values. + +If both `id` and `name` flag values are provided, `id` is preferred. + +For more information about Registered Resources, see the manual for the `registered-resources` subcommand. + +## Example + +Get by ID: + +```shell +otdfctl policy registered-resources get --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` + +Get by Name: + +```shell +otdfctl policy registered-resources get --name=my_resource +``` diff --git a/docs/man/policy/registered-resources/list.md b/docs/man/policy/registered-resources/list.md new file mode 100644 index 00000000..f945da98 --- /dev/null +++ b/docs/man/policy/registered-resources/list.md @@ -0,0 +1,22 @@ +--- +title: List Registered Resources +command: + name: list + aliases: + - l + flags: + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +For more information about Registered Resources, see the `registered-resources` subcommand. + +## Example + +```shell +otdfctl policy registered-resources list +``` diff --git a/docs/man/policy/registered-resources/update.md b/docs/man/policy/registered-resources/update.md new file mode 100644 index 00000000..11b4d43a --- /dev/null +++ b/docs/man/policy/registered-resources/update.md @@ -0,0 +1,36 @@ +--- +title: Update a Registered Resource +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the registered resource to update + required: true + - name: name + shorthand: n + description: Optional updated name of the registered resource (must be unique within Policy) + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' + - name: force-replace-labels + description: Destructively replace entire set of existing metadata 'labels' with any provided to this command + default: false +--- + +Update the `name` and/or metadata labels for a Registered Resource. + +If PEPs rely on this registered resource name, a name update could break access. + +Make sure you know what you are doing. + +For more information about Registered Resources, see the `registered-resources` subcommand. + +## Example + +```shell +otdfctl policy registered-resources update --id 34c62145-5d99-45cb-a732-13cb16270e63 --name new_resource_name +``` diff --git a/docs/man/policy/registered-resources/values/_index.md b/docs/man/policy/registered-resources/values/_index.md new file mode 100644 index 00000000..5e44b5da --- /dev/null +++ b/docs/man/policy/registered-resources/values/_index.md @@ -0,0 +1,11 @@ +--- +title: Manage Registered Resource Values +command: + name: values + aliases: + - val + - value + hidden: true +--- + +Registered Resource Values are the values associated with a registered resource. diff --git a/docs/man/policy/registered-resources/values/create.md b/docs/man/policy/registered-resources/values/create.md new file mode 100644 index 00000000..67d6b632 --- /dev/null +++ b/docs/man/policy/registered-resources/values/create.md @@ -0,0 +1,40 @@ +--- +title: Create Registered Resource Value +command: + name: create + aliases: + - c + - add + - new + flags: + - name: resource + shorthand: r + description: Identifier of the associated registered resource (ID or name) + required: true + - name: value + shorthand: v + description: Value of the registered resource (i.e. 'value1', must be unique within the Registered Resource) + required: true + - name: action-attribute-value + shorthand: a + description: "Optional action attribute values in the format: \";\"" + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Add a value to a registered resource in the platform Policy. + +A registered resource value `value` is normalized to lower case and may contain hyphens or dashes between other alphanumeric characters. + +For more information, see the `registered-resources` subcommand. + +## Examples + +Create a registered resource value for the registered resource with ID '3c51a593-cbf8-419d-b7dc-b656d0bedfbb', value 'my_value', and action attribute values using action/attribute value IDs, action names, and attribute value FQNs: + +```shell +otdfctl policy registered-resources values create --resource 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value my_value --action-attribute-value "74a3eade-ef6c-4422-b764-fe0471f5c6c1;405a35a7-2051-49a6-9645-3a667b4739f3" --action-attribute-value "create;https://example.com/attr/my_attribute/value/my_value" +``` diff --git a/docs/man/policy/registered-resources/values/delete.md b/docs/man/policy/registered-resources/values/delete.md new file mode 100644 index 00000000..a7d31ce3 --- /dev/null +++ b/docs/man/policy/registered-resources/values/delete.md @@ -0,0 +1,24 @@ +--- +title: Delete a Registered Resource Value +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the registered resource value + required: true + - name: force + description: Force deletion without interactive confirmation +--- + +Removes a Registered Resource Value from platform Policy. + +Registered resource value deletion cascades to the associated Action Attribute Values. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +```shell +otdfctl policy registered-resources values delete --id 217b300a-47f9-4bee-be8c-d38c880053f7 +``` diff --git a/docs/man/policy/registered-resources/values/get.md b/docs/man/policy/registered-resources/values/get.md new file mode 100644 index 00000000..31cdac09 --- /dev/null +++ b/docs/man/policy/registered-resources/values/get.md @@ -0,0 +1,34 @@ +--- +title: Get a Registered Resource Value +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the registered resource value + - name: fqn + shorthand: f + description: FQN of the registered resource value +--- + +Retrieve a registered resource value along with its metadata. + +If both `id` and `fqn` flag values are provided, `id` is preferred. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +Get by ID: + +```shell +otdfctl policy registered-resources values get --id=3c51a593-cbf8-419d-b7dc-b656d0bedfbb +``` + +Get by FQN: + +```shell +otdfctl policy registered-resources values get --fqn=https://reg_res/my_name/value/my_value +``` diff --git a/docs/man/policy/registered-resources/values/list.md b/docs/man/policy/registered-resources/values/list.md new file mode 100644 index 00000000..519953c7 --- /dev/null +++ b/docs/man/policy/registered-resources/values/list.md @@ -0,0 +1,27 @@ +--- +title: List Registered Resource Values +command: + name: list + aliases: + - l + flags: + - name: resource + shorthand: r + description: Identifier of the associated registered resource (ID or name) + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +List registered resource values in the platform Policy. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +```shell +otdfctl policy registered-resources values list +``` diff --git a/docs/man/policy/registered-resources/values/update.md b/docs/man/policy/registered-resources/values/update.md new file mode 100644 index 00000000..4a14bb90 --- /dev/null +++ b/docs/man/policy/registered-resources/values/update.md @@ -0,0 +1,38 @@ +--- +title: Update a Registered Resource Value +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the registered resource value to update + - name: value + shorthand: v + description: Optional updated value of the registered resource value (must be unique within the Registered Resource) + - name: action-attribute-value + shorthand: a + description: "Optional action attribute values in the format: \";\"" + default: '' + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: '' +--- + +Update any or all of the `value`, action attribute values, and metadata labels for a Registered Resource Value. + +If PEPs rely on this value, a value update could break access. + +Updating the action attribute values will remove and replace all existing action attribute values for this registered resource value. + +Make sure you know what you are doing. + +For more information about Registered Resource Values, see the manual for the `values` subcommand. + +## Example + +```shell +otdfctl policy registered-resources values update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --value new_value --action-attribute-value "74a3eade-ef6c-4422-b764-fe0471f5c6c1;405a35a7-2051-49a6-9645-3a667b4739f3" --action-attribute-value "create;https://example.com/attr/my_attribute/value/my_value" +``` diff --git a/e2e/registered-resources.bats b/e2e/registered-resources.bats new file mode 100644 index 00000000..2bd5f26a --- /dev/null +++ b/e2e/registered-resources.bats @@ -0,0 +1,423 @@ +#!/usr/bin/env bats + +# Tests for registered resources + +setup_file() { + echo -n '{"clientId":"opentdf","clientSecret":"secret"}' > creds.json + export WITH_CREDS='--with-client-creds-file ./creds.json' + export HOST='--host http://localhost:8080' + + # create registered resource used in registered resource values tests + export RR_NAME="test_rr_for_values" + export RR_ID=$(./otdfctl $HOST $WITH_CREDS policy registered-resources create --name "$RR_NAME" --json | jq -r '.id') + + # create custom action to be used in registered resource values tests + export CUSTOM_ACTION_NAME="test_action_for_values" + export CUSTOM_ACTION_ID=$(./otdfctl $HOST $WITH_CREDS policy actions create --name "$CUSTOM_ACTION_NAME" --json | jq -r '.id') + + # get standard read action id to use in registered resource values tests + export READ_ACTION_NAME="read" + export READ_ACTION_ID=$(./otdfctl $HOST $WITH_CREDS policy actions get --name "$READ_ACTION_NAME" --json | jq -r '.id') + + # create attribute value to be used in registered resource values tests + export NS_NAME="test-rr.org" + export NS_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes namespaces create --name "$NS_NAME" --json | jq -r '.id') + export ATTR_NAME=test_rr_attr + attr_id=$(./otdfctl $HOST $WITH_CREDS policy attributes create --namespace "$NS_ID" --name "$ATTR_NAME" --rule ANY_OF -l key=value --json | jq -r '.id') + export ATTR_VAL_1_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$attr_id" --value test_reg_res_attr__val_1 --json | jq -r '.id') + export ATTR_VAL_1_FQN=$(./otdfctl $HOST $WITH_CREDS policy attributes values get --id "$ATTR_VAL_1_ID" --json | jq -r '.fqn') + export ATTR_VAL_2_ID=$(./otdfctl $HOST $WITH_CREDS policy attributes values create --attribute-id "$attr_id" --value test_reg_res_attr__val_2 --json | jq -r '.id') + export ATTR_VAL_2_FQN=$(./otdfctl $HOST $WITH_CREDS policy attributes values get --id "$ATTR_VAL_2_ID" --json | jq -r '.fqn') + + echo "FQN: $ATTR_VAL_1_FQN" +} + +setup() { + load "${BATS_LIB_PATH}/bats-support/load.bash" + load "${BATS_LIB_PATH}/bats-assert/load.bash" + + # invoke binary with credentials + run_otdfctl_reg_res () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy registered-resources $*" + } + run_otdfctl_reg_res_values () { + run sh -c "./otdfctl $HOST $WITH_CREDS policy registered-resources values $*" + } +} + +teardown_file() { + # remove the registered resource used in registered resource values tests + ./otdfctl $HOST $WITH_CREDS policy registered-resources delete --id "$RR_ID" --force + + # remove the custom action used in registered resource values tests + ./otdfctl $HOST $WITH_CREDS policy actions delete --id "$CUSTOM_ACTION_ID" --force + + # remove the namespace and cascade delete attributes and values used in registered resource values tests + ./otdfctl $HOST $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_ID" --force + + # clear out all test env vars + unset HOST WITH_CREDS RR_NAME RR_ID CUSTOM_ACTION_NAME CUSTOM_ACTION_ID READ_ACTION_NAME READ_ACTION_ID NS_NAME NS_ID ATTR_NAME ATTR_VAL_1_ID ATTR_VAL_1_FQN ATTR_VAL_2_ID ATTR_VAL_2_FQN +} + +@test "Create a registered resource - Good" { + run_otdfctl_reg_res create --name test_create_rr + assert_output --partial "SUCCESS" + assert_line --regexp "Name.*test_create_rr" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + + # cleanup + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Create a registered resource - Bad" { + # bad resource names + run_otdfctl_reg_res create --name ends_underscored_ + assert_failure + run_otdfctl_reg_res create --name -first-char-hyphen + assert_failure + run_otdfctl_reg_res create --name inval!d.chars + assert_failure + + # missing flag + run_otdfctl_reg_res create + assert_failure + assert_output --partial "Flag '--name' is required" + + # conflict + run_otdfctl_reg_res create --name test_create_rr_conflict + assert_output --partial "SUCCESS" + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res create --name test_create_rr_conflict + assert_failure + assert_output --partial "already_exists" + + # cleanup + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Get a registered resource - Good" { + # setup a resource to get + run_otdfctl_reg_res create --name test_get_rr + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # get by id + run_otdfctl_reg_res get --id "$created_id" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_rr" ] + + # get by name + run_otdfctl_reg_res get --name test_get_rr --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.name')" = "test_get_rr" ] + + # cleanup + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Get a registered resource - Bad" { + run_otdfctl_reg_res get + assert_failure + assert_output --partial "Either 'id' or 'name' must be provided" + + run_otdfctl_reg_res get --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" +} + +@test "List registered resources" { + # setup registered resources to list + run_otdfctl_reg_res create --name test_list_rr_1 + reg_res1_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res create --name test_list_rr_2 + reg_res2_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + run_otdfctl_reg_res list + assert_success + assert_output --partial "$reg_res1_id" + assert_output --partial "test_list_rr_1" + assert_output --partial "$reg_res2_id" + assert_output --partial "test_list_rr_2" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + # cleanup + run_otdfctl_reg_res delete --id $reg_res1_id --force + run_otdfctl_reg_res delete --id $reg_res2_id --force +} + +@test "Update registered resource" { + # setup a resource to update + run_otdfctl_reg_res create --name test_update_rr + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # force replace labels + run_otdfctl_reg_res update --id "$created_id" -l key=other --force-replace-labels + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Name.*test_update_rr" + assert_line --regexp "Labels.*key: other" + refute_output --regexp "Labels.*key: value" + refute_output --regexp "Labels.*test: true" + refute_output --regexp "Labels.*test: true" + + # renamed + run_otdfctl_reg_res update --id "$created_id" --name test_renamed_rr + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Name.*test_renamed_rr" + refute_output --regexp "Name.*test_update_rr" + + # cleanup + run_otdfctl_reg_res delete --id $created_id --force +} + +@test "Delete registered resource - Good" { + # setup a resource to delete + run_otdfctl_reg_res create --name test_delete_rr + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + run_otdfctl_reg_res delete --id "$created_id" --force + assert_success +} + +@test "Delete registered resource - Bad" { + # no id + run_otdfctl_reg_res delete + assert_failure + assert_output --partial "Flag '--id' is required" + + # invalid id + run_otdfctl_reg_res delete --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" +} + +# Tests for registered resource values + +@test "Create a registered resource value - Good" { + # simple by resource ID + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val + assert_output --partial "SUCCESS" + assert_line --regexp "Value.*test_create_rr_val" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + created_id_simple=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # simple by resource name + run_otdfctl_reg_res_values create --resource "$RR_NAME" --value test_create_rr_val_by_res_name + assert_output --partial "SUCCESS" + assert_line --regexp "Value.*test_create_rr_val" + assert_output --partial "Id" + assert_output --partial "Created At" + assert_line --partial "Updated At" + created_id_simple_by_res_name=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # with action attribute values + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_with_action_attr_vals --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_FQN\"" --action-attribute-value "\"$CUSTOM_ACTION_NAME;$ATTR_VAL_2_ID\"" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" != "" ] + [ "$(echo "$output" | jq -r '.value')" = "test_create_rr_val_with_action_attr_vals" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$READ_ACTION_ID"'" and .action.name == "'"$READ_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_1_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_1_FQN"'")')" = "true" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$CUSTOM_ACTION_ID"'" and .action.name == "'"$CUSTOM_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_2_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_2_FQN"'")')" = "true" ] + created_id_with_action_attr_vals=$(echo "$output" | jq -r '.id') + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id_simple --force + run_otdfctl_reg_res_values delete --id $created_id_simple_by_res_name --force + run_otdfctl_reg_res_values delete --id $created_id_with_action_attr_vals --force +} + +@test "Create a registered resource value - Bad" { + # bad resource value names + run_otdfctl_reg_res_values create --resource "$RR_ID" --value ends_underscored_ + assert_failure + run_otdfctl_reg_res_values create --resource "$RR_ID" --value -first-char-hyphen + assert_failure + run_otdfctl_reg_res_values create --resource "$RR_ID" --value inval!d.chars + assert_failure + + # missing flag + run_otdfctl_reg_res_values create + assert_failure + assert_output --partial "Flag '--resource' is required" + run_otdfctl_reg_res_values create --resource "$RR_ID" + assert_failure + assert_output --partial "Flag '--value' is required" + + # bad action attribute value arg separator (not a semicolon) + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_bad_aav --action-attribute-value "\"$READ_ACTION_ID:$ATTR_VAL_1_ID\"" + assert_failure + assert_output --partial "Invalid action attribute value arg format" + + # non-existent resource name + run_otdfctl_reg_res_values create --resource invalid_rr --value test_create_rr_val_bad_aav_action_name + assert_failure + assert_output --partial "Failed to find registered resource (name: invalid_rr)" + + # conflict + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_conflict + assert_output --partial "SUCCESS" + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_create_rr_val_conflict + assert_failure + assert_output --partial "already_exists" + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id --force +} + +@test "Get a registered resource value - Good" { + # setup a resource value to get + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_get_rr_val --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_ID\"" + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # get by id + run_otdfctl_reg_res_values get --id "$created_id" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.value')" = "test_get_rr_val" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.id')" = "$READ_ACTION_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.name')" = "$READ_ACTION_NAME" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.id')" = "$ATTR_VAL_1_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.fqn')" = "$ATTR_VAL_1_FQN" ] + + # get by fqn + run_otdfctl_reg_res_values get --fqn "https://reg_res/$RR_NAME/value/test_get_rr_val" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r '.value')" = "test_get_rr_val" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.id')" = "$READ_ACTION_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].action.name')" = "$READ_ACTION_NAME" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.id')" = "$ATTR_VAL_1_ID" ] + [ "$(echo "$output" | jq -r '.action_attribute_values[0].attribute_value.fqn')" = "$ATTR_VAL_1_FQN" ] + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id --force +} + +@test "Get a registered resource value - Bad" { + run_otdfctl_reg_res_values get + assert_failure + assert_output --partial "Either 'id' or 'fqn' must be provided" + + # invalud id + run_otdfctl_reg_res_values get --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" + + # invalid fqn + run_otdfctl_reg_res_values get --fqn 'not_a_fqn' + assert_failure + assert_output --partial "must be a valid URI" +} + +@test "List registered resource values - Good" { + # setup values to list + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_list_rr_val_1 --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_ID\"" + reg_res_val1_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_list_rr_val_2 + reg_res_val2_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # by resource ID + run_otdfctl_reg_res_values list --resource "$RR_ID" + assert_success + assert_output --partial "$reg_res_val1_id" + assert_output --partial "test_list_rr_val_1" + # check for partial FQN due to possible trimmed output + assert_output --partial "$READ_ACTION_NAME -> https://$NS_NAME/attr/$ATTR_NAME" + assert_output --partial "$reg_res_val2_id" + assert_output --partial "test_list_rr_val_2" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + # by resource name + run_otdfctl_reg_res_values list --resource "$RR_NAME" + assert_success + assert_output --partial "$reg_res_val1_id" + assert_output --partial "test_list_rr_val_1" + # check for partial FQN due to possible trimmed output + assert_output --partial "$READ_ACTION_NAME -> https://$NS_NAME/attr/$ATTR_NAME" + assert_output --partial "$reg_res_val2_id" + assert_output --partial "test_list_rr_val_2" + assert_output --partial "Total" + assert_line --regexp "Current Offset.*0" + + # cleanup + run_otdfctl_reg_res_values delete --id $reg_res_val1_id --force + run_otdfctl_reg_res_values delete --id $reg_res_val2_id --force +} + +@test "List registered resource values - Bad" { + # non-existent resource name + run_otdfctl_reg_res_values list --resource 'invalid_rr' + assert_failure + assert_output --partial "Failed to find registered resource (name: invalid_rr)" +} + +@test "Update registered resource values" { + # setup a resource value to update + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_update_rr_val --action-attribute-value "\"$READ_ACTION_ID;$ATTR_VAL_1_ID\"" + assert_success + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + # force replace labels + run_otdfctl_reg_res_values update --id "$created_id" -l key=other --force-replace-labels + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Value.*test_update_rr_val" + assert_line --regexp "Labels.*key: other" + refute_output --regexp "Labels.*key: value" + refute_output --regexp "Labels.*test: true" + refute_output --regexp "Labels.*test: true" + + # renamed + run_otdfctl_reg_res_values update --id "$created_id" --value test_renamed_rr_val + assert_success + assert_line --regexp "Id.*$created_id" + assert_line --regexp "Value.*test_renamed_rr_val" + refute_output --regexp "Value.*test_update_rr_val" + + # ensure previous updates without action attribute value args did not clear action attribute values + run_otdfctl_reg_res_values get --id "$created_id" --json + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$READ_ACTION_ID"'" and .action.name == "'"$READ_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_1_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_1_FQN"'")')" = "true" ] + + # update action attribute values + run_otdfctl_reg_res_values update --id "$created_id" --action-attribute-value "\"$READ_ACTION_NAME;$ATTR_VAL_1_FQN\"" --action-attribute-value "\"$CUSTOM_ACTION_ID;$ATTR_VAL_2_ID\"" --json + assert_success + [ "$(echo "$output" | jq -r '.id')" = "$created_id" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$READ_ACTION_ID"'" and .action.name == "'"$READ_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_1_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_1_FQN"'")')" = "true" ] + [ "$(echo "$output" | jq -r 'any(.action_attribute_values[]; .action.id == "'"$CUSTOM_ACTION_ID"'" and .action.name == "'"$CUSTOM_ACTION_NAME"'" and .attribute_value.id == "'"$ATTR_VAL_2_ID"'" and .attribute_value.fqn == "'"$ATTR_VAL_2_FQN"'")')" = "true" ] + + # cleanup + run_otdfctl_reg_res_values delete --id $created_id --force +} + +@test "Delete registered resource value - Good" { + # setup a value to delete + run_otdfctl_reg_res_values create --resource "$RR_ID" --value test_delete_rr_val + created_id=$(echo "$output" | grep Id | awk -F'│' '{print $3}' | xargs) + + run_otdfctl_reg_res_values delete --id "$created_id" --force + assert_success +} + +@test "Delete registered resource value - Bad" { + # no id + run_otdfctl_reg_res_values delete + assert_failure + assert_output --partial "Flag '--id' is required" + + # invalid id + run_otdfctl_reg_res_values delete --id 'not_a_uuid' + assert_failure + assert_output --partial "must be a valid UUID" +} \ No newline at end of file diff --git a/pkg/cli/sdkHelpers.go b/pkg/cli/sdkHelpers.go index 04906667..76348a12 100644 --- a/pkg/cli/sdkHelpers.go +++ b/pkg/cli/sdkHelpers.go @@ -2,6 +2,7 @@ package cli import ( "strconv" + "strings" "time" "github.com/opentdf/otdfctl/pkg/handlers" @@ -77,3 +78,30 @@ func GetSimpleAttributeValue(v *policy.Value) SimpleAttributeValue { Metadata: ConstructMetadata(v.GetMetadata()), } } + +func GetSimpleRegisteredResourceValues(v []*policy.RegisteredResourceValue) []string { + values := make([]string, len(v)) + for i, val := range v { + values[i] = val.GetValue() + } + return values +} + +func GetSimpleRegisteredResourceActionAttributeValues(v []*policy.RegisteredResourceValue_ActionAttributeValue) []string { + values := make([]string, len(v)) + sb := new(strings.Builder) + + for i, val := range v { + action := val.GetAction() + attrVal := val.GetAttributeValue() + + sb.WriteString(action.GetName()) + sb.WriteString(" -> ") + sb.WriteString(attrVal.GetFqn()) + + values[i] = sb.String() + sb.Reset() + } + + return values +} diff --git a/pkg/handlers/registeredResources.go b/pkg/handlers/registeredResources.go new file mode 100644 index 00000000..f0ba9869 --- /dev/null +++ b/pkg/handlers/registeredResources.go @@ -0,0 +1,158 @@ +package handlers + +import ( + "context" + + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" +) + +// +// Registered Resources +// + +func (h Handler) CreateRegisteredResource(ctx context.Context, name string, values []string, metadata *common.MetadataMutable) (*policy.RegisteredResource, error) { + resp, err := h.sdk.RegisteredResources.CreateRegisteredResource(ctx, ®isteredresources.CreateRegisteredResourceRequest{ + Name: name, + Values: values, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + + return resp.GetResource(), nil +} + +func (h Handler) GetRegisteredResource(ctx context.Context, id, name string) (*policy.RegisteredResource, error) { + req := ®isteredresources.GetRegisteredResourceRequest{} + if id != "" { + req.Identifier = ®isteredresources.GetRegisteredResourceRequest_Id{ + Id: id, + } + } else { + req.Identifier = ®isteredresources.GetRegisteredResourceRequest_Name{ + Name: name, + } + } + + resp, err := h.sdk.RegisteredResources.GetRegisteredResource(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetResource(), nil +} + +func (h Handler) ListRegisteredResources(ctx context.Context, limit, offset int32) ([]*policy.RegisteredResource, *policy.PageResponse, error) { + resp, err := h.sdk.RegisteredResources.ListRegisteredResources(ctx, ®isteredresources.ListRegisteredResourcesRequest{ + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + }) + if err != nil { + return nil, nil, err + } + + return resp.GetResources(), resp.GetPagination(), nil +} + +func (h Handler) UpdateRegisteredResource(ctx context.Context, id, name string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.RegisteredResource, error) { + _, err := h.sdk.RegisteredResources.UpdateRegisteredResource(ctx, ®isteredresources.UpdateRegisteredResourceRequest{ + Id: id, + Name: name, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetRegisteredResource(ctx, id, "") +} + +func (h Handler) DeleteRegisteredResource(ctx context.Context, id string) error { + _, err := h.sdk.RegisteredResources.DeleteRegisteredResource(ctx, ®isteredresources.DeleteRegisteredResourceRequest{ + Id: id, + }) + + return err +} + +// +// Registered Resource Values +// + +func (h Handler) CreateRegisteredResourceValue(ctx context.Context, resourceID string, value string, actionAttributeValues []*registeredresources.ActionAttributeValue, metadata *common.MetadataMutable) (*policy.RegisteredResourceValue, error) { + resp, err := h.sdk.RegisteredResources.CreateRegisteredResourceValue(ctx, ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: resourceID, + Value: value, + ActionAttributeValues: actionAttributeValues, + Metadata: metadata, + }) + if err != nil { + return nil, err + } + + return resp.GetValue(), nil +} + +func (h Handler) GetRegisteredResourceValue(ctx context.Context, id, fqn string) (*policy.RegisteredResourceValue, error) { + req := ®isteredresources.GetRegisteredResourceValueRequest{} + if id != "" { + req.Identifier = ®isteredresources.GetRegisteredResourceValueRequest_Id{ + Id: id, + } + } else { + req.Identifier = ®isteredresources.GetRegisteredResourceValueRequest_Fqn{ + Fqn: fqn, + } + } + + resp, err := h.sdk.RegisteredResources.GetRegisteredResourceValue(ctx, req) + if err != nil { + return nil, err + } + + return resp.GetValue(), nil +} + +func (h Handler) ListRegisteredResourceValues(ctx context.Context, resourceID string, limit, offset int32) ([]*policy.RegisteredResourceValue, *policy.PageResponse, error) { + resp, err := h.sdk.RegisteredResources.ListRegisteredResourceValues(ctx, ®isteredresources.ListRegisteredResourceValuesRequest{ + ResourceId: resourceID, + Pagination: &policy.PageRequest{ + Limit: limit, + Offset: offset, + }, + }) + if err != nil { + return nil, nil, err + } + + return resp.GetValues(), resp.GetPagination(), nil +} + +func (h Handler) UpdateRegisteredResourceValue(ctx context.Context, id, value string, actionAttributeValues []*registeredresources.ActionAttributeValue, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.RegisteredResourceValue, error) { + _, err := h.sdk.RegisteredResources.UpdateRegisteredResourceValue(ctx, ®isteredresources.UpdateRegisteredResourceValueRequest{ + Id: id, + Value: value, + ActionAttributeValues: actionAttributeValues, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + return nil, err + } + + return h.GetRegisteredResourceValue(ctx, id, "") +} + +func (h Handler) DeleteRegisteredResourceValue(ctx context.Context, id string) error { + _, err := h.sdk.RegisteredResources.DeleteRegisteredResourceValue(ctx, ®isteredresources.DeleteRegisteredResourceValueRequest{ + Id: id, + }) + + return err +}