diff --git a/cmd/policy-attributes.go b/cmd/policy-attributes.go index 31236394..4a3684db 100644 --- a/cmd/policy-attributes.go +++ b/cmd/policy-attributes.go @@ -5,6 +5,7 @@ import ( "github.com/evertras/bubble-table/table" "github.com/opentdf/otdfctl/pkg/cli" + "github.com/opentdf/otdfctl/pkg/handlers" "github.com/opentdf/otdfctl/pkg/man" "github.com/opentdf/platform/protocol/go/policy" "github.com/spf13/cobra" @@ -184,6 +185,117 @@ func policy_updateAttribute(cmd *cobra.Command, args []string) { } } +func policy_unsafeReactivateAttribute(cmd *cobra.Command, args []string) { + h := NewHandler(cmd) + defer h.Close() + + flagHelper := cli.NewFlagHelper(cmd) + id := flagHelper.GetRequiredString("id") + + a, err := h.GetAttribute(id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionReactivate, "attribute", cli.InputNameFQN, a.GetFqn()) + } + + if a, err := h.UnsafeReactivateAttribute(id); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to reactivate attribute (%s)", id), err) + } else { + rows := [][]string{ + {"Id", a.Id}, + {"Name", a.Name}, + } + if mdRows := getMetadataRows(a.Metadata); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + HandleSuccess(cmd, id, t, a) + } +} + +func policy_unsafeUpdateAttribute(cmd *cobra.Command, args []string) { + h := NewHandler(cmd) + defer h.Close() + + flagHelper := cli.NewFlagHelper(cmd) + id := flagHelper.GetRequiredString("id") + name := flagHelper.GetOptionalString("name") + rule := flagHelper.GetOptionalString("rule") + valuesOrder := flagHelper.GetStringSlice("values-order", attrValues, cli.FlagHelperStringSliceOptions{}) + + a, err := h.GetAttribute(id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionUpdateUnsafe, "attribute", cli.InputNameFQN, a.GetFqn()) + } + + if err := h.UnsafeUpdateAttribute(id, name, rule, valuesOrder); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to update attribute (%s)", id), err) + } else { + var ( + values []string + valueIDs []string + ) + for _, v := range a.GetValues() { + values = append(values, v.GetValue()) + valueIDs = append(valueIDs, v.GetId()) + } + rows := [][]string{ + {"Id", a.Id}, + {"Name", a.GetName()}, + {"Rule", handlers.GetAttributeRuleFromAttributeType(a.GetRule())}, + {"Values", cli.CommaSeparated(values)}, + {"Value IDs", cli.CommaSeparated(valueIDs)}, + } + if mdRows := getMetadataRows(a.Metadata); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + HandleSuccess(cmd, id, t, a) + } +} + +func policy_unsafeDeleteAttribute(cmd *cobra.Command, args []string) { + h := NewHandler(cmd) + defer h.Close() + + flagHelper := cli.NewFlagHelper(cmd) + id := flagHelper.GetRequiredString("id") + + a, err := h.GetAttribute(id) + if err != nil { + errMsg := fmt.Sprintf("Failed to get attribute (%s)", id) + cli.ExitWithError(errMsg, err) + } + + if !forceUnsafe { + cli.ConfirmTextInput(cli.ActionDelete, "attribute", cli.InputNameFQN, a.GetFqn()) + } + + if err := h.UnsafeDeleteAttribute(id, a.GetFqn()); err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to delete attribute (%s)", id), err) + } else { + rows := [][]string{ + {"Deleted", "true"}, + {"Id", a.Id}, + {"Name", a.Name}, + } + if mdRows := getMetadataRows(a.Metadata); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + HandleSuccess(cmd, id, t, a) + } +} + func init() { // Create an attribute createDoc := man.Docs.GetCommand("policy/attributes/create", @@ -261,6 +373,62 @@ func init() { deactivateDoc.GetDocFlag("id").Description, ) - policy_attributesCmd.AddSubcommands(createDoc, getDoc, listDoc, updateDoc, deactivateDoc) + // unsafe actions on attributes + unsafeCmd := man.Docs.GetCommand("policy/attributes/unsafe") + unsafeCmd.PersistentFlags().BoolVar(&forceUnsafe, + unsafeCmd.GetDocFlag("force").Name, + false, + unsafeCmd.GetDocFlag("force").Description, + ) + + reactivateCmd := man.Docs.GetCommand("policy/attributes/unsafe/reactivate", + man.WithRun(policy_unsafeReactivateAttribute), + ) + reactivateCmd.Flags().StringP( + reactivateCmd.GetDocFlag("id").Name, + reactivateCmd.GetDocFlag("id").Shorthand, + reactivateCmd.GetDocFlag("id").Default, + reactivateCmd.GetDocFlag("id").Description, + ) + deleteCmd := man.Docs.GetCommand("policy/attributes/unsafe/delete", + man.WithRun(policy_unsafeDeleteAttribute), + ) + deleteCmd.Flags().StringP( + deleteCmd.GetDocFlag("id").Name, + deleteCmd.GetDocFlag("id").Shorthand, + deleteCmd.GetDocFlag("id").Default, + deleteCmd.GetDocFlag("id").Description, + ) + unsafeUpdateCmd := man.Docs.GetCommand("policy/attributes/unsafe/update", + man.WithRun(policy_unsafeUpdateAttribute), + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("id").Name, + unsafeUpdateCmd.GetDocFlag("id").Shorthand, + unsafeUpdateCmd.GetDocFlag("id").Default, + unsafeUpdateCmd.GetDocFlag("id").Description, + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("name").Name, + unsafeUpdateCmd.GetDocFlag("name").Shorthand, + unsafeUpdateCmd.GetDocFlag("name").Default, + unsafeUpdateCmd.GetDocFlag("name").Description, + ) + unsafeUpdateCmd.Flags().StringP( + unsafeUpdateCmd.GetDocFlag("rule").Name, + unsafeUpdateCmd.GetDocFlag("rule").Shorthand, + unsafeUpdateCmd.GetDocFlag("rule").Default, + unsafeUpdateCmd.GetDocFlag("rule").Description, + ) + unsafeUpdateCmd.Flags().StringSliceVarP( + &attrValues, + unsafeUpdateCmd.GetDocFlag("values-order").Name, + unsafeUpdateCmd.GetDocFlag("values-order").Shorthand, + []string{}, + unsafeUpdateCmd.GetDocFlag("values-order").Description, + ) + + unsafeCmd.AddSubcommands(reactivateCmd, deleteCmd, unsafeUpdateCmd) + policy_attributesCmd.AddSubcommands(createDoc, getDoc, listDoc, updateDoc, deactivateDoc, unsafeCmd) policyCmd.AddCommand(&policy_attributesCmd.Command) } diff --git a/docs/man/policy/attributes/create.md b/docs/man/policy/attributes/create.md index 1f46db31..4ad6b58a 100644 --- a/docs/man/policy/attributes/create.md +++ b/docs/man/policy/attributes/create.md @@ -17,14 +17,14 @@ command: required: true - name: value shorthand: v - description: Value of the attribute + description: Value of the attribute (i.e. 'value1') required: true - name: namespace shorthand: s - description: Namespace of the attribute + description: Namespace ID of the attribute required: true - name: label description: "Optional metadata 'labels' in the format: key=value" shorthand: l - default: "" + default: '' --- diff --git a/docs/man/policy/attributes/unsafe/_index.md b/docs/man/policy/attributes/unsafe/_index.md new file mode 100644 index 00000000..bbe63683 --- /dev/null +++ b/docs/man/policy/attributes/unsafe/_index.md @@ -0,0 +1,19 @@ +--- +title: Unsafe changes to attribute definitions +command: + name: unsafe + flags: + - name: force + description: Force unsafe change without confirmation + required: false +--- + +# Unsafe Changes to Attribute Definitions + +Unsafe changes are dangerous mutations to Policy that can significantly change access behavior around existing attributes +and entitlement. + +Depending on the unsafe change introduced and already existing TDFs, TDFs might become inaccessible that were previously +accessible or vice versa. + +Make sure you know what you are doing. diff --git a/docs/man/policy/attributes/unsafe/delete.md b/docs/man/policy/attributes/unsafe/delete.md new file mode 100644 index 00000000..c2ecf37a --- /dev/null +++ b/docs/man/policy/attributes/unsafe/delete.md @@ -0,0 +1,18 @@ +--- +title: Delete an attribute definition +command: + name: delete + flags: + - name: id + shorthand: i + description: ID of the attribute definition + required: true +--- + +# Unsafe Delete Warning + +Deleting an Attribute Definition cascades deletion of any Attribute Values and any associated mappings underneath. + +Any existing TDFs containing the deleted attribute of this name will be rendered inaccessible until it has been recreated. + +Make sure you know what you are doing. diff --git a/docs/man/policy/attributes/unsafe/reactivate.md b/docs/man/policy/attributes/unsafe/reactivate.md new file mode 100644 index 00000000..e1f69a8b --- /dev/null +++ b/docs/man/policy/attributes/unsafe/reactivate.md @@ -0,0 +1,18 @@ +--- +title: Reactivate an attribute definition +command: + name: reactivate + flags: + - name: id + shorthand: i + description: ID of the attribute definition + required: true +--- + +# Unsafe Reactivate Warning + +Reactivating an Attribute Definition can potentially open up an access path to any existing TDFs referencing values under that definition. + +The Active/Inactive state of any Attribute Values under this Definition will NOT be changed. + +Make sure you know what you are doing. diff --git a/docs/man/policy/attributes/unsafe/update.md b/docs/man/policy/attributes/unsafe/update.md new file mode 100644 index 00000000..10090ccd --- /dev/null +++ b/docs/man/policy/attributes/unsafe/update.md @@ -0,0 +1,48 @@ +--- +title: Update an attribute definition +command: + name: update + flags: + - name: id + shorthand: i + description: ID of the attribute definition + required: true + - name: name + shorthand: n + description: Name of the attribute definition + - name: rule + shorthand: r + description: Rule of the attribute definition + enum: + - ANY_OF + - ALL_OF + - HIERARCHY + - name: values-order + shorthand: o + description: Order of the attribute values (IDs) +--- + +# Unsafe Update Warning + +## Name Update + +Renaming an Attribute Definition means any Values and any associated mappings underneath will now be tied to the new name. + +Any existing TDFs containing attributes under the old definition name will be rendered inaccessible, and any TDFs tied to the new name +and already created may now become accessible. + +## Rule Update + +Altering a rule of an Attribute Definition changes the evaluation of entitlement to data. Existing TDFs of the same definition name +and values will now be accessible based on the updated rule. An `anyOf` rule becoming `hierarchy` or vice versa, for example, have +entirely different meanings and access evaluations. + +## Values-Order Update + +In the case of a `hierarchy` Attribute Definition Rule, the order of Values on the attribute has significant impact on data access. +Changing this order (complete, destructive replacement of the existing order) will impact access to data. + +To remove Values from an Attribute Definition, delete them separately via the `values unsafe` commands. To add, utilize safe +`values create` commands. + +Make sure you know what you are doing. diff --git a/pkg/handlers/attribute.go b/pkg/handlers/attribute.go index d4f7357b..24edcf05 100644 --- a/pkg/handlers/attribute.go +++ b/pkg/handlers/attribute.go @@ -6,6 +6,7 @@ import ( "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/unsafe" ) // TODO: Might be useful to map out the attribute rule definitions for help text in the CLI and TUI @@ -100,6 +101,48 @@ func (h Handler) DeactivateAttribute(id string) (*policy.Attribute, error) { return h.GetAttribute(id) } +// Reactivates and returns reactivated attribute +func (h Handler) UnsafeReactivateAttribute(id string) (*policy.Attribute, error) { + _, err := h.sdk.Unsafe.UnsafeReactivateAttribute(h.ctx, &unsafe.UnsafeReactivateAttributeRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + return h.GetAttribute(id) +} + +// Deletes and returns error if deletion failed +func (h Handler) UnsafeDeleteAttribute(id, fqn string) error { + _, err := h.sdk.Unsafe.UnsafeDeleteAttribute(h.ctx, &unsafe.UnsafeDeleteAttributeRequest{ + Id: id, + Fqn: fqn, + }) + return err +} + +// Deletes and returns error if deletion failed +func (h Handler) UnsafeUpdateAttribute(id, name, rule string, values_order []string) error { + req := &unsafe.UnsafeUpdateAttributeRequest{ + Id: id, + Name: name, + } + + if rule != "" { + r, err := GetAttributeRuleFromReadableString(rule) + if err != nil { + return fmt.Errorf("invalid attribute rule: %s", rule) + } + req.Rule = r + } + if len(values_order) > 0 { + req.ValuesOrder = values_order + } + + _, err := h.sdk.Unsafe.UnsafeUpdateAttribute(h.ctx, req) + return err +} + func GetAttributeFqn(namespace string, name string) string { return fmt.Sprintf("https://%s/attr/%s", namespace, name) } @@ -112,6 +155,7 @@ func GetAttributeRuleOptions() []string { } } +// Provides the un-prefixed human-readable attribute rule func GetAttributeRuleFromAttributeType(rule policy.AttributeRuleTypeEnum) string { switch rule { case policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF: