diff --git a/cmd/kas-public-keys.go b/cmd/kas-public-keys.go new file mode 100644 index 00000000..27548891 --- /dev/null +++ b/cmd/kas-public-keys.go @@ -0,0 +1,541 @@ +package cmd + +import ( + "encoding/base64" + "errors" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/term" + "github.com/evertras/bubble-table/table" + "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/kasregistry" + "github.com/spf13/cobra" +) + +var policy_kasPublicKeyCmd = man.Docs.GetCommand("policy/kas-registry/public-keys") + +func policy_createPublicKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + kas := c.Flags.GetRequiredString("kas") + publicKey := c.Flags.GetRequiredString("key") + alg := c.Flags.GetRequiredString("algorithm") + kid := c.Flags.GetRequiredString("key-id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + + publicKey, err := parseAndFormatKey(publicKey) + if err != nil { + cli.ExitWithError("Failed to parse public key", err) + } + + created, err := h.CreatePublicKey(kas, publicKey, kid, alg, getMetadataMutable(metadataLabels)) + if err != nil { + cli.ExitWithError("Failed to create public key", err) + } + + rows := [][]string{ + {"Id", created.GetId()}, + {"Key ID", created.GetPublicKey().GetKid()}, + {"Algorithm", alg}, + {"Public Key", created.GetPublicKey().GetPem()}, + {"Was Mapped", created.GetWasMapped().String()}, + {"Active", created.GetIsActive().String()}, + } + + if mdRows := getMetadataRows(created.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, created.GetId(), t, created) +} + +func policy_updatePublicKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) + updated, err := h.UpdatePublicKey(id, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior()) + if err != nil { + cli.ExitWithError("Failed to update public key", err) + } + + rows := [][]string{ + {"Id", updated.GetId()}, + {"Key ID", updated.GetPublicKey().GetKid()}, + } + + if mdRows := getMetadataRows(updated.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, updated.GetId(), t, updated) +} + +func policy_activePublicKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + err := h.ActivatePublicKey(id) + if err != nil { + cli.ExitWithError("Failed to active public key", err) + } + + HandleSuccess(cmd, id, table.Model{}, nil) +} + +func policy_deactivePublicKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + err := h.DeactivatePublicKey(id) + if err != nil { + cli.ExitWithError("Failed to deactivate public key", err) + } + + HandleSuccess(cmd, id, table.Model{}, nil) +} + +func policy_unsafeDeletePublicKey(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") + + pk, err := h.GetPublicKey(id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get public key with id %s", id), err) + } + + if !force { + cli.ConfirmTextInput(cli.ActionDelete, "public-key", cli.InputNameKeyID, pk.GetPublicKey().GetKid()) + } + + err = h.UnsafeDeletePublicKey(id) + if err != nil { + cli.ExitWithError("Failed to delete public key", err) + } + + rows := [][]string{ + {"Id", pk.GetId()}, + {"Key ID", pk.GetPublicKey().GetKid()}, + } + if mdRows := getMetadataRows(pk.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + t := cli.NewTabular(rows...) + HandleSuccess(cmd, pk.GetId(), t, pk) +} + +func policy_getPublicKey(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + id := c.Flags.GetRequiredID("id") + + key, err := h.GetPublicKey(id) + if err != nil { + cli.ExitWithError(fmt.Sprintf("Failed to get public key with id %s", id), err) + } + + alg, err := enumToAlg(key.GetPublicKey().GetAlg()) + if err != nil { + cli.ExitWithError("Failed to get algorithm", err) + } + + rows := [][]string{ + {"Id", key.GetId()}, + {"Was Mapped", fmt.Sprintf("%t", key.GetWasMapped().GetValue())}, + {"Active", fmt.Sprintf("%t", key.GetIsActive().GetValue())}, + {"KAS Name", key.GetKas().GetName()}, + {"KAS URI", key.GetKas().GetUri()}, + {"Key ID", key.GetPublicKey().GetKid()}, + {"Algorithm", alg}, + {"Public Key", key.GetPublicKey().GetPem()}, + } + + if mdRows := getMetadataRows(key.GetMetadata()); mdRows != nil { + rows = append(rows, mdRows...) + } + + cli.NewTable( + cli.NewUUIDColumn(), + ) + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, key.GetId(), t, key) +} + +func policy_listPublicKeys(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + kas := c.Flags.GetOptionalString("kas") + showPublicKey := c.Flags.GetOptionalBool("show-public-key") + offset := c.Flags.GetRequiredInt32("offset") + limit := c.Flags.GetRequiredInt32("limit") + + keys, page, err := h.ListPublicKeys(kas, offset, limit) + if err != nil { + cli.ExitWithError("Failed to list public keys", err) + } + + columns := []table.Column{ + table.NewFlexColumn("id", "ID", cli.FlexColumnWidthThree), + table.NewFlexColumn("is_active", "Active", cli.FlexColumnWidthTwo), + table.NewFlexColumn("was_mapped", "Was Mapped", cli.FlexColumnWidthTwo), + table.NewFlexColumn("kas_name", "KAS Name", cli.FlexColumnWidthThree), + table.NewFlexColumn("kas_uri", "KAS URI", cli.FlexColumnWidthThree), + table.NewFlexColumn("key_id", "Key ID", cli.FlexColumnWidthTwo), + table.NewFlexColumn("algorithm", "Algorithm", cli.FlexColumnWidthTwo), + } + + if showPublicKey { + columns = append(columns, table.NewFlexColumn("public_key", "Public Key", cli.FlexColumnWidthFour)) + } + + t := cli.NewTable(columns...) + + rows := []table.Row{} + for _, key := range keys { + alg, err := enumToAlg(key.GetPublicKey().GetAlg()) + if err != nil { + cli.ExitWithError("Failed to get algorithm", err) + } + + rowStyle := lipgloss.NewStyle().BorderBottom(true).BorderStyle(lipgloss.NormalBorder()) + + if key.GetIsActive().GetValue() { + rowStyle = rowStyle.Background(cli.ColorGreen.Background) + } else { + rowStyle = rowStyle.Background(cli.ColorRed.Background) + } + + rd := table.RowData{ + "id": key.GetId(), + "is_active": key.GetIsActive().GetValue(), + "was_mapped": key.GetWasMapped().GetValue(), + "kas_id": key.GetKas().GetId(), + "kas_name": key.GetKas().GetName(), + "kas_uri": key.GetKas().GetUri(), + "key_id": key.GetPublicKey().GetKid(), + "algorithm": alg, + "public_key": key.GetPublicKey().GetPem(), + } + + rows = append(rows, table.NewRow(rd).WithStyle(rowStyle)) + } + + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, page) + + HandleSuccess(cmd, "", t, keys) +} + +func policy_listPublicKeyMappings(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + kas := c.Flags.GetOptionalString("kas") + pkID := c.Flags.GetOptionalID("public-key-id") + offset := c.Flags.GetRequiredInt32("offset") + limit := c.Flags.GetRequiredInt32("limit") + + mappings, page, err := h.ListPublicKeyMappings(kas, pkID, offset, limit) + if err != nil { + cli.ExitWithError("Failed to list public key mappings", err) + } + + t := cli.NewTable( + table.NewFlexColumn("kas_name", "KAS Name", cli.FlexColumnWidthTwo), + table.NewFlexColumn("kas_uri", "KAS URI", cli.FlexColumnWidthThree), + table.NewFlexColumn("key_count", "Key Count", cli.FlexColumnWidthOne), + table.NewFlexColumn("publicKeys", "Public Keys", cli.FlexColumnWidthFour), + ) + + rows := []table.Row{} + + termWidth, _, err := term.GetSize(os.Stdout.Fd()) + if err != nil { + termWidth = 80 + } + + for _, mapping := range mappings { + rows = append(rows, table.NewRow(table.RowData{ + "kas_name": mapping.GetKasName(), + "kas_uri": mapping.GetKasUri(), + "key_count": fmt.Sprintf("%d", len(mapping.GetPublicKeys())), + "publicKeys": createPublicKeysTable(mapping.GetPublicKeys(), termWidth), + }).WithStyle(lipgloss.NewStyle().BorderBottom(true).BorderStyle(lipgloss.NormalBorder()))) + } + + t = t.WithRows(rows) + t = cli.WithListPaginationFooter(t, page) + + HandleSuccess(cmd, "", t, mappings) +} + +func createPublicKeysTable(keys []*kasregistry.ListPublicKeyMappingResponse_PublicKey, termWidth int) string { + // Create columns for the nested table + columns := []table.Column{ + table.NewFlexColumn("kid", "KID", cli.FlexColumnWidthTwo), + table.NewFlexColumn("algorithm", "Algorithm", cli.FlexColumnWidthTwo), + table.NewFlexColumn("active", "Active", cli.FlexColumnWidthOne), + table.NewFlexColumn("namespaces", "Namespaces", cli.FlexColumnWidthTwo), + table.NewFlexColumn("definitions", "Definitions", cli.FlexColumnWidthThree), + table.NewFlexColumn("values", "Values", cli.FlexColumnWidthFour), + } + + var rows []table.Row + + for _, pk := range keys { + alg, err := enumToAlg(pk.GetKey().GetPublicKey().GetAlg()) + if err != nil { + cli.ExitWithError("Failed to get algorithm", err) + } + + rowStyle := lipgloss.NewStyle().BorderBottom(true).BorderStyle(lipgloss.NormalBorder()) + + if pk.GetKey().GetIsActive().GetValue() { + rowStyle = rowStyle.Background(cli.ColorGreen.Background) + } else { + rowStyle = rowStyle.Background(cli.ColorRed.Background) + } + + rows = append(rows, table.NewRow(table.RowData{ + "kid": pk.GetKey().GetPublicKey().GetKid(), + "algorithm": alg, + "active": fmt.Sprintf("%v", pk.GetKey().GetIsActive().GetValue()), + "namespaces": formatAssociations(pk.GetNamespaces()), + "definitions": formatAssociations(pk.GetDefinitions()), + "values": formatAssociations(pk.GetValues()), + }).WithStyle(rowStyle)) + } + + minWidth := 80 // Set a minimum width for the nested table + tableWidthPercentage := 0.75 + tableWidth := int(float64(termWidth) * tableWidthPercentage) + if tableWidth < minWidth { + tableWidth = minWidth + } + // Create nested table + nestedTable := table.New(columns). + WithRows(rows). + WithTargetWidth(int(float64(tableWidth) * tableWidthPercentage)). + WithMultiline(true). + WithNoPagination(). + BorderRounded(). + WithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Left)) + + // Convert the table to string and add some indentation + tableStr := nestedTable.View() + + // Add indentation to each line of the nested table + indentedLines := strings.Split(tableStr, "\n") + for i, line := range indentedLines { + indentedLines[i] = " " + line + } + return strings.Join(indentedLines, "\n") +} + +func formatAssociations(assocs []*kasregistry.ListPublicKeyMappingResponse_Association) string { + if len(assocs) == 0 { + return "-" + } + var fqns []string + for _, a := range assocs { + // remove https:// from the beginning of the URI + fqn, _ := strings.CutPrefix(a.GetFqn(), "https://") + fqns = append(fqns, fqn) + } + return strings.Join(fqns, "\n") +} + +func isValidBase64(s string) bool { + _, err := base64.StdEncoding.DecodeString(s) + return err == nil +} + +func parseAndFormatKey(key string) (string, error) { + if key == "" { + return "", errors.New("key is required") + } + + // If the key contains a newline, replace it with the actual newline character + if strings.Contains(key, "\\n") { + return strings.ReplaceAll(key, "\\n", "\n"), nil + } + + // If the key is base64 encoded, decode it + if isValidBase64(key) { + decoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", err + } + return string(decoded), nil + } + + return key, nil +} + +func enumToAlg(enum policy.KasPublicKeyAlgEnum) (string, error) { + switch enum { //nolint:exhaustive // UNSPECIFIED is not needed here + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048: + return "rsa:2048", nil + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096: + return "rsa:4096", nil + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1: + return "ec:secp256r1", nil + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1: + return "ec:secp384r1", nil + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1: + return "ec:secp521r1", nil + default: + return "", errors.New("invalid enum algorithm") + } +} + +func init() { + createDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/create", + man.WithRun(policy_createPublicKey)) + createDoc.Flags().StringP( + createDoc.GetDocFlag("kas").Name, + createDoc.GetDocFlag("kas").Shorthand, + createDoc.GetDocFlag("kas").Default, + createDoc.GetDocFlag("kas").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("key").Name, + createDoc.GetDocFlag("key").Shorthand, + createDoc.GetDocFlag("key").Default, + createDoc.GetDocFlag("key").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("algorithm").Name, + createDoc.GetDocFlag("algorithm").Shorthand, + createDoc.GetDocFlag("algorithm").Default, + createDoc.GetDocFlag("algorithm").Description, + ) + createDoc.Flags().StringP( + createDoc.GetDocFlag("key-id").Name, + createDoc.GetDocFlag("key-id").Shorthand, + createDoc.GetDocFlag("key-id").Default, + createDoc.GetDocFlag("key-id").Description, + ) + injectLabelFlags(&createDoc.Command, false) + + updateDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/update", + man.WithRun(policy_updatePublicKey)) + updateDoc.Flags().StringP( + updateDoc.GetDocFlag("id").Name, + updateDoc.GetDocFlag("id").Shorthand, + updateDoc.GetDocFlag("id").Default, + updateDoc.GetDocFlag("id").Description, + ) + injectLabelFlags(&updateDoc.Command, true) + + activateDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/activate", + man.WithRun(policy_activePublicKey)) + activateDoc.Flags().StringP( + activateDoc.GetDocFlag("id").Name, + activateDoc.GetDocFlag("id").Shorthand, + activateDoc.GetDocFlag("id").Default, + activateDoc.GetDocFlag("id").Description, + ) + + deactivateDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/deactivate", + man.WithRun(policy_deactivePublicKey)) + deactivateDoc.Flags().StringP( + deactivateDoc.GetDocFlag("id").Name, + deactivateDoc.GetDocFlag("id").Shorthand, + deactivateDoc.GetDocFlag("id").Default, + deactivateDoc.GetDocFlag("id").Description, + ) + + getDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/get", + man.WithRun(policy_getPublicKey)) + getDoc.Flags().StringP( + getDoc.GetDocFlag("id").Name, + getDoc.GetDocFlag("id").Shorthand, + getDoc.GetDocFlag("id").Default, + getDoc.GetDocFlag("id").Description, + ) + + listDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/list", + man.WithRun(policy_listPublicKeys)) + listDoc.Flags().StringP( + listDoc.GetDocFlag("kas").Name, + listDoc.GetDocFlag("kas").Shorthand, + listDoc.GetDocFlag("kas").Default, + listDoc.GetDocFlag("kas").Description, + ) + listDoc.Flags().BoolP( + listDoc.GetDocFlag("show-public-key").Name, + listDoc.GetDocFlag("show-public-key").Shorthand, + listDoc.GetDocFlag("show-public-key").DefaultAsBool(), + listDoc.GetDocFlag("show-public-key").Description, + ) + injectListPaginationFlags(listDoc) + + listMappingsDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/list-mappings", + man.WithRun(policy_listPublicKeyMappings)) + listMappingsDoc.Flags().StringP( + listMappingsDoc.GetDocFlag("kas").Name, + listMappingsDoc.GetDocFlag("kas").Shorthand, + listMappingsDoc.GetDocFlag("kas").Default, + listMappingsDoc.GetDocFlag("kas").Description, + ) + listMappingsDoc.Flags().StringP( + listMappingsDoc.GetDocFlag("public-key-id").Name, + listMappingsDoc.GetDocFlag("public-key-id").Shorthand, + listMappingsDoc.GetDocFlag("public-key-id").Default, + listMappingsDoc.GetDocFlag("public-key-id").Description, + ) + injectListPaginationFlags(listMappingsDoc) + + unsafeDeleteDoc := man.Docs.GetCommand("policy/kas-registry/public-keys/unsafe/delete", + man.WithRun(policy_unsafeDeletePublicKey)) + unsafeDeleteDoc.Flags().StringP( + unsafeDeleteDoc.GetDocFlag("id").Name, + unsafeDeleteDoc.GetDocFlag("id").Shorthand, + unsafeDeleteDoc.GetDocFlag("id").Default, + unsafeDeleteDoc.GetDocFlag("id").Description, + ) + unsafeDeleteDoc.Flags().BoolP( + unsafeDeleteDoc.GetDocFlag("force").Name, + unsafeDeleteDoc.GetDocFlag("force").Shorthand, + unsafeDeleteDoc.GetDocFlag("force").DefaultAsBool(), + unsafeDeleteDoc.GetDocFlag("force").Description, + ) + + policy_kasPublicKeyUnsafeCmd := man.Docs.GetCommand("policy/kas-registry/public-keys/unsafe") + policy_kasPublicKeyUnsafeCmd.AddSubcommands(unsafeDeleteDoc) + + policy_kasPublicKeyCmd.AddCommand(&policy_kasPublicKeyUnsafeCmd.Command) + + policy_kasPublicKeyCmd.AddSubcommands(createDoc, updateDoc, getDoc, listDoc, listMappingsDoc, activateDoc, deactivateDoc) + policy_kasRegistryCmd.AddCommand(&policy_kasPublicKeyCmd.Command) +} diff --git a/cmd/kas-registry.go b/cmd/kas-registry.go index ee67e46d..9fb2912b 100644 --- a/cmd/kas-registry.go +++ b/cmd/kas-registry.go @@ -14,7 +14,7 @@ import ( "google.golang.org/protobuf/encoding/protojson" ) -var policy_kasRegistryCmd *cobra.Command +var policy_kasRegistryCmd = man.Docs.GetCommand("policy/kas-registry") func policy_getKeyAccessRegistry(cmd *cobra.Command, args []string) { c := cli.New(cmd, args) @@ -34,6 +34,7 @@ func policy_getKeyAccessRegistry(cmd *cobra.Command, args []string) { if kas.GetPublicKey().GetRemote() != "" { key.PublicKey = &policy.PublicKey_Remote{Remote: kas.GetPublicKey().GetRemote()} } + rows := [][]string{ {"Id", kas.GetId()}, {"URI", kas.GetUri()}, @@ -69,7 +70,6 @@ func policy_listKeyAccessRegistries(cmd *cobra.Command, args []string) { cli.NewUUIDColumn(), table.NewFlexColumn("uri", "URI", cli.FlexColumnWidthFour), table.NewFlexColumn("name", "Name", cli.FlexColumnWidthThree), - table.NewFlexColumn("pk", "PublicKey", cli.FlexColumnWidthFour), ) rows := []table.Row{} for _, kas := range list { @@ -102,10 +102,6 @@ func policy_createKeyAccessRegistry(cmd *cobra.Command, args []string) { name := c.Flags.GetOptionalString("name") metadataLabels = c.Flags.GetStringSlice("label", metadataLabels, cli.FlagsStringSliceOptions{Min: 0}) - if cachedJSON == "" && remote == "" { - cli.ExitWithError("Empty flags 'public-keys' and 'public-key-remote'", errors.New("error: a public key is required")) - } - key := new(policy.PublicKey) if cachedJSON != "" { if remote != "" { @@ -353,9 +349,6 @@ func init() { deleteDoc.GetDocFlag("force").Description, ) - doc := man.Docs.GetCommand("policy/kas-registry", - man.WithSubcommands(createDoc, getDoc, listDoc, updateDoc, deleteDoc), - ) - policy_kasRegistryCmd = &doc.Command - policyCmd.AddCommand(policy_kasRegistryCmd) + policy_kasRegistryCmd.AddSubcommands(createDoc, getDoc, listDoc, updateDoc, deleteDoc) + policyCmd.AddCommand(&policy_kasRegistryCmd.Command) } diff --git a/cmd/policy-attributeNamespaces.go b/cmd/policy-attributeNamespaces.go index 16dfde76..ca4183a6 100644 --- a/cmd/policy-attributeNamespaces.go +++ b/cmd/policy-attributeNamespaces.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" + "github.com/charmbracelet/lipgloss" "github.com/evertras/bubble-table/table" "github.com/opentdf/otdfctl/pkg/cli" "github.com/opentdf/otdfctl/pkg/man" @@ -12,6 +13,7 @@ import ( var ( policy_attributeNamespacesCmd = man.Docs.GetCommand("policy/attributes/namespaces") + policy_NamespaceKeysCmd = man.Docs.GetCommand("policy/attributes/namespaces/keys") forceUnsafe bool ) @@ -265,6 +267,107 @@ func policy_unsafeUpdateAttributeNamespace(cmd *cobra.Command, args []string) { HandleSuccess(cmd, ns.GetId(), t, ns) } +func policy_NamespaceKeysAdd(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + ns := c.Flags.GetRequiredString("namespace") + pkID := c.Flags.GetRequiredID("public-key-id") + + _, err := h.AddPublicKeyToNamespace(c.Context(), ns, pkID) + if err != nil { + cli.ExitWithError("Failed to add public key to namespace", err) + } + + rows := [][]string{ + {"Public Key Id", pkID}, + {"Namespace", ns}, + } + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, "Public key added to namespace", t, nil) +} + +func policy_NamespaceKeysRemove(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + ns := c.Flags.GetRequiredString("namespace") + pkID := c.Flags.GetRequiredID("public-key-id") + + _, err := h.RemovePublicKeyFromNamespace(c.Context(), ns, pkID) + if err != nil { + cli.ExitWithError("Failed to remove public key from namespace", err) + } + + rows := [][]string{ + {"Public Key Id", pkID}, + {"Namespace", ns}, + } + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, "Public key removed from namespace", t, nil) +} + +func policy_NamespaceKeysListcmd(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + ns := c.Flags.GetRequiredString("namespace") + showPublicKey := c.Flags.GetOptionalBool("show-public-key") + + list, err := h.GetNamespace(ns) + if err != nil { + cli.ExitWithError("Failed to list namespace keys", err) + } + + columns := []table.Column{ + table.NewFlexColumn("kas_name", "KAS Name", cli.FlexColumnWidthThree), + table.NewFlexColumn("kas_uri", "KAS URI", cli.FlexColumnWidthThree), + table.NewFlexColumn("kid", "Key ID", cli.FlexColumnWidthThree), + table.NewFlexColumn("alg", "Algorithm", cli.FlexColumnWidthThree), + } + + if showPublicKey { + columns = append(columns, table.NewFlexColumn("public_key", "Public Key", cli.FlexColumnWidthFour)) + } + + t := cli.NewTable(columns...) + rows := []table.Row{} + for _, key := range list.GetKeys() { + alg, err := enumToAlg(key.GetPublicKey().GetAlg()) + if err != nil { + cli.ExitWithError("Failed to get algorithm", err) + } + + rowStyle := lipgloss.NewStyle().BorderBottom(true).BorderStyle(lipgloss.NormalBorder()) + + if key.GetIsActive().GetValue() { + rowStyle = rowStyle.Background(cli.ColorGreen.Background) + } else { + rowStyle = rowStyle.Background(cli.ColorRed.Background) + } + + rd := table.RowData{ + "key_id": key.GetPublicKey().GetKid(), + "algorithm": alg, + "is_active": key.GetIsActive().GetValue(), + "kas_id": key.GetKas().GetId(), + "kas_name": key.GetKas().GetName(), + "kas_uri": key.GetKas().GetUri(), + "public_key": key.GetPublicKey().GetPem(), + } + rows = append(rows, table.NewRow(rd).WithStyle(rowStyle)) + } + t = t.WithRows(rows) + HandleSuccess(cmd, "", t, list) +} + func init() { getCmd := man.Docs.GetCommand("policy/attributes/namespaces/get", man.WithRun(policy_getAttributeNamespace), @@ -367,6 +470,55 @@ func init() { ) unsafeCmd.AddSubcommands(deleteCmd, reactivateCmd, unsafeUpdateCmd) - policy_attributeNamespacesCmd.AddSubcommands(getCmd, listCmd, createDoc, updateCmd, deactivateCmd, unsafeCmd) + namespaceKeysAddDoc := man.Docs.GetCommand("policy/attributes/namespaces/keys/add", + man.WithRun(policy_NamespaceKeysAdd), + ) + namespaceKeysAddDoc.Flags().StringP( + namespaceKeysAddDoc.GetDocFlag("namespace").Name, + namespaceKeysAddDoc.GetDocFlag("namespace").Shorthand, + namespaceKeysAddDoc.GetDocFlag("namespace").Default, + namespaceKeysAddDoc.GetDocFlag("namespace").Description, + ) + namespaceKeysAddDoc.Flags().StringP( + namespaceKeysAddDoc.GetDocFlag("public-key-id").Name, + namespaceKeysAddDoc.GetDocFlag("public-key-id").Shorthand, + namespaceKeysAddDoc.GetDocFlag("public-key-id").Default, + namespaceKeysAddDoc.GetDocFlag("public-key-id").Description, + ) + + namespaceKeysRemoveDoc := man.Docs.GetCommand("policy/attributes/namespaces/keys/remove", + man.WithRun(policy_NamespaceKeysRemove), + ) + namespaceKeysRemoveDoc.Flags().StringP( + namespaceKeysRemoveDoc.GetDocFlag("namespace").Name, + namespaceKeysRemoveDoc.GetDocFlag("namespace").Shorthand, + namespaceKeysRemoveDoc.GetDocFlag("namespace").Default, + namespaceKeysRemoveDoc.GetDocFlag("namespace").Description, + ) + namespaceKeysRemoveDoc.Flags().StringP( + namespaceKeysRemoveDoc.GetDocFlag("public-key-id").Name, + namespaceKeysRemoveDoc.GetDocFlag("public-key-id").Shorthand, + namespaceKeysRemoveDoc.GetDocFlag("public-key-id").Default, + namespaceKeysRemoveDoc.GetDocFlag("public-key-id").Description, + ) + + namespaceKeysListDoc := man.Docs.GetCommand("policy/attributes/namespaces/keys/list", + man.WithRun(policy_NamespaceKeysListcmd), + ) + namespaceKeysListDoc.Flags().StringP( + namespaceKeysListDoc.GetDocFlag("namespace").Name, + namespaceKeysListDoc.GetDocFlag("namespace").Shorthand, + namespaceKeysListDoc.GetDocFlag("namespace").Default, + namespaceKeysListDoc.GetDocFlag("namespace").Description, + ) + namespaceKeysListDoc.Flags().BoolP( + namespaceKeysListDoc.GetDocFlag("show-public-key").Name, + namespaceKeysListDoc.GetDocFlag("show-public-key").Shorthand, + namespaceKeysListDoc.GetDocFlag("show-public-key").DefaultAsBool(), + namespaceKeysListDoc.GetDocFlag("show-public-key").Description, + ) + + policy_NamespaceKeysCmd.AddSubcommands(namespaceKeysAddDoc, namespaceKeysRemoveDoc, namespaceKeysListDoc) + policy_attributeNamespacesCmd.AddSubcommands(getCmd, listCmd, createDoc, updateCmd, deactivateCmd, unsafeCmd, policy_NamespaceKeysCmd) policy_attributesCmd.AddCommand(&policy_attributeNamespacesCmd.Command) } diff --git a/cmd/policy-attributeValues.go b/cmd/policy-attributeValues.go index 633ede4c..7240c96c 100644 --- a/cmd/policy-attributeValues.go +++ b/cmd/policy-attributeValues.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/charmbracelet/lipgloss" "github.com/evertras/bubble-table/table" "github.com/opentdf/otdfctl/pkg/cli" "github.com/opentdf/otdfctl/pkg/man" @@ -10,7 +11,10 @@ import ( "github.com/spf13/cobra" ) -var policy_attributeValuesCmd *cobra.Command +var ( + policy_attributeValuesCmd *cobra.Command + policy_ValueKeysCmd = man.Docs.GetCommand("policy/attributes/values/keys") +) func policy_createAttributeValue(cmd *cobra.Command, args []string) { c := cli.New(cmd, args) @@ -226,6 +230,107 @@ func policy_unsafeDeleteAttributeValue(cmd *cobra.Command, args []string) { } } +func policy_ValueKeysAdd(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + val := c.Flags.GetRequiredString("value") + pkID := c.Flags.GetRequiredID("public-key-id") + + ak, err := h.AddPublicKeyToValue(c.Context(), val, pkID) + if err != nil { + cli.ExitWithError("Failed to add public key to value", err) + } + + rows := [][]string{ + {"Public Key Id", pkID}, + {"Attribute Value", val}, + } + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, "Public key added to value", t, ak) +} + +func policy_ValueKeysRemove(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + val := c.Flags.GetRequiredString("value") + pkID := c.Flags.GetRequiredID("public-key-id") + + _, err := h.RemovePublicKeyFromValue(c.Context(), val, pkID) + if err != nil { + cli.ExitWithError("Failed to remove public key from value", err) + } + + rows := [][]string{ + {"Public Key Id", pkID}, + {"Attribute Value", val}, + } + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, "Public key removed from value", t, nil) +} + +func policy_ValueKeysList(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + ns := c.Flags.GetRequiredString("value") + showPublicKey := c.Flags.GetOptionalBool("show-public-key") + + list, err := h.GetAttributeValue(ns) + if err != nil { + cli.ExitWithError("Failed to list attribute value keys", err) + } + + columns := []table.Column{ + table.NewFlexColumn("kas_name", "KAS Name", cli.FlexColumnWidthThree), + table.NewFlexColumn("kas_uri", "KAS URI", cli.FlexColumnWidthThree), + table.NewFlexColumn("kid", "Key ID", cli.FlexColumnWidthThree), + table.NewFlexColumn("alg", "Algorithm", cli.FlexColumnWidthThree), + } + + if showPublicKey { + columns = append(columns, table.NewFlexColumn("public_key", "Public Key", cli.FlexColumnWidthFour)) + } + + t := cli.NewTable(columns...) + rows := []table.Row{} + for _, key := range list.GetKeys() { + alg, err := enumToAlg(key.GetPublicKey().GetAlg()) + if err != nil { + cli.ExitWithError("Failed to get algorithm", err) + } + + rowStyle := lipgloss.NewStyle().BorderBottom(true).BorderStyle(lipgloss.NormalBorder()) + + if key.GetIsActive().GetValue() { + rowStyle = rowStyle.Background(cli.ColorGreen.Background) + } else { + rowStyle = rowStyle.Background(cli.ColorRed.Background) + } + + rd := table.RowData{ + "key_id": key.GetPublicKey().GetKid(), + "algorithm": alg, + "is_active": key.GetIsActive().GetValue(), + "kas_id": key.GetKas().GetId(), + "kas_name": key.GetKas().GetName(), + "kas_uri": key.GetKas().GetUri(), + "public_key": key.GetPublicKey().GetPem(), + } + rows = append(rows, table.NewRow(rd).WithStyle(rowStyle)) + } + t = t.WithRows(rows) + HandleSuccess(cmd, "", t, list) +} + func init() { createCmd := man.Docs.GetCommand("policy/attributes/values/create", man.WithRun(policy_createAttributeValue), @@ -335,9 +440,59 @@ func init() { unsafeCmd.GetDocFlag("force").Description, ) + valueKeysAddDoc := man.Docs.GetCommand("policy/attributes/values/keys/add", + man.WithRun(policy_ValueKeysAdd), + ) + valueKeysAddDoc.Flags().StringP( + valueKeysAddDoc.GetDocFlag("value").Name, + valueKeysAddDoc.GetDocFlag("value").Shorthand, + valueKeysAddDoc.GetDocFlag("value").Default, + valueKeysAddDoc.GetDocFlag("value").Description, + ) + valueKeysAddDoc.Flags().StringP( + valueKeysAddDoc.GetDocFlag("public-key-id").Name, + valueKeysAddDoc.GetDocFlag("public-key-id").Shorthand, + valueKeysAddDoc.GetDocFlag("public-key-id").Default, + valueKeysAddDoc.GetDocFlag("public-key-id").Description, + ) + + valueKeysRemoveDoc := man.Docs.GetCommand("policy/attributes/values/keys/remove", + man.WithRun(policy_ValueKeysRemove), + ) + valueKeysRemoveDoc.Flags().StringP( + valueKeysRemoveDoc.GetDocFlag("value").Name, + valueKeysRemoveDoc.GetDocFlag("value").Shorthand, + valueKeysRemoveDoc.GetDocFlag("value").Default, + valueKeysRemoveDoc.GetDocFlag("value").Description, + ) + valueKeysRemoveDoc.Flags().StringP( + valueKeysRemoveDoc.GetDocFlag("public-key-id").Name, + valueKeysRemoveDoc.GetDocFlag("public-key-id").Shorthand, + valueKeysRemoveDoc.GetDocFlag("public-key-id").Default, + valueKeysRemoveDoc.GetDocFlag("public-key-id").Description, + ) + + valueKeysListDoc := man.Docs.GetCommand("policy/attributes/values/keys/list", + man.WithRun(policy_ValueKeysList), + ) + valueKeysListDoc.Flags().StringP( + valueKeysListDoc.GetDocFlag("value").Name, + valueKeysListDoc.GetDocFlag("value").Shorthand, + valueKeysListDoc.GetDocFlag("value").Default, + valueKeysListDoc.GetDocFlag("value").Description, + ) + valueKeysListDoc.Flags().BoolP( + valueKeysListDoc.GetDocFlag("show-public-key").Name, + valueKeysListDoc.GetDocFlag("show-public-key").Shorthand, + valueKeysListDoc.GetDocFlag("show-public-key").DefaultAsBool(), + valueKeysListDoc.GetDocFlag("show-public-key").Description, + ) + + policy_ValueKeysCmd.AddSubcommands(valueKeysAddDoc, valueKeysRemoveDoc, valueKeysListDoc) + unsafeCmd.AddSubcommands(unsafeReactivateCmd, unsafeDeleteCmd, unsafeUpdateCmd) doc := man.Docs.GetCommand("policy/attributes/values", - man.WithSubcommands(createCmd, getCmd, listCmd, updateCmd, deactivateCmd, unsafeCmd), + man.WithSubcommands(createCmd, getCmd, listCmd, updateCmd, deactivateCmd, unsafeCmd, policy_ValueKeysCmd), ) policy_attributeValuesCmd = &doc.Command policy_attributesCmd.AddCommand(policy_attributeValuesCmd) diff --git a/cmd/policy-attributes.go b/cmd/policy-attributes.go index 8a3dfc74..bedb24a5 100644 --- a/cmd/policy-attributes.go +++ b/cmd/policy-attributes.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/charmbracelet/lipgloss" "github.com/evertras/bubble-table/table" "github.com/opentdf/otdfctl/pkg/cli" "github.com/opentdf/otdfctl/pkg/handlers" @@ -16,7 +17,8 @@ var ( values []string valuesOrder []string - policy_attributesCmd = man.Docs.GetCommand("policy/attributes") + policy_attributesCmd = man.Docs.GetCommand("policy/attributes") + policy_DefinitionKeysCmd = man.Docs.GetCommand("policy/attributes/keys") ) func policy_createAttribute(cmd *cobra.Command, args []string) { @@ -300,6 +302,107 @@ func policy_unsafeDeleteAttribute(cmd *cobra.Command, args []string) { } } +func policy_DefinitionKeysAdd(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + def := c.Flags.GetRequiredString("definition") + pkID := c.Flags.GetRequiredID("public-key-id") + + ak, err := h.AddPublicKeyToDefinition(c.Context(), def, pkID) + if err != nil { + cli.ExitWithError("Failed to add public key to definition", err) + } + + rows := [][]string{ + {"Public Key Id", pkID}, + {"Attribute Definition", def}, + } + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, "Public key added to definition", t, ak) +} + +func policy_DefinitionKeysRemove(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + def := c.Flags.GetRequiredString("definition") + pkID := c.Flags.GetRequiredID("public-key-id") + + _, err := h.RemovePublicKeyFromDefinition(c.Context(), def, pkID) + if err != nil { + cli.ExitWithError("Failed to remove public key from definition", err) + } + + rows := [][]string{ + {"Public Key Id", pkID}, + {"Attribute Definition", def}, + } + + t := cli.NewTabular(rows...) + + HandleSuccess(cmd, "Public key removed from definition", t, nil) +} + +func policy_DefinitionKeysList(cmd *cobra.Command, args []string) { + c := cli.New(cmd, args) + h := NewHandler(c) + defer h.Close() + + ns := c.Flags.GetRequiredString("definition") + showPublicKey := c.Flags.GetOptionalBool("show-public-key") + + list, err := h.GetAttribute(ns) + if err != nil { + cli.ExitWithError("Failed to list definition keys", err) + } + + columns := []table.Column{ + table.NewFlexColumn("kas_name", "KAS Name", cli.FlexColumnWidthThree), + table.NewFlexColumn("kas_uri", "KAS URI", cli.FlexColumnWidthThree), + table.NewFlexColumn("kid", "Key ID", cli.FlexColumnWidthThree), + table.NewFlexColumn("alg", "Algorithm", cli.FlexColumnWidthThree), + } + + if showPublicKey { + columns = append(columns, table.NewFlexColumn("public_key", "Public Key", cli.FlexColumnWidthFour)) + } + + t := cli.NewTable(columns...) + rows := []table.Row{} + for _, key := range list.GetKeys() { + alg, err := enumToAlg(key.GetPublicKey().GetAlg()) + if err != nil { + cli.ExitWithError("Failed to get algorithm", err) + } + + rowStyle := lipgloss.NewStyle().BorderBottom(true).BorderStyle(lipgloss.NormalBorder()) + + if key.GetIsActive().GetValue() { + rowStyle = rowStyle.Background(cli.ColorGreen.Background) + } else { + rowStyle = rowStyle.Background(cli.ColorRed.Background) + } + + rd := table.RowData{ + "key_id": key.GetPublicKey().GetKid(), + "algorithm": alg, + "is_active": key.GetIsActive().GetValue(), + "kas_id": key.GetKas().GetId(), + "kas_name": key.GetKas().GetName(), + "kas_uri": key.GetKas().GetUri(), + "public_key": key.GetPublicKey().GetPem(), + } + rows = append(rows, table.NewRow(rd).WithStyle(rowStyle)) + } + t = t.WithRows(rows) + HandleSuccess(cmd, "", t, list) +} + func init() { // Create an attribute createDoc := man.Docs.GetCommand("policy/attributes/create", @@ -438,7 +541,57 @@ func init() { unsafeUpdateCmd.GetDocFlag("values-order").Description, ) + definitionKeysAddDoc := man.Docs.GetCommand("policy/attributes/keys/add", + man.WithRun(policy_DefinitionKeysAdd), + ) + definitionKeysAddDoc.Flags().StringP( + definitionKeysAddDoc.GetDocFlag("definition").Name, + definitionKeysAddDoc.GetDocFlag("definition").Shorthand, + definitionKeysAddDoc.GetDocFlag("definition").Default, + definitionKeysAddDoc.GetDocFlag("definition").Description, + ) + definitionKeysAddDoc.Flags().StringP( + definitionKeysAddDoc.GetDocFlag("public-key-id").Name, + definitionKeysAddDoc.GetDocFlag("public-key-id").Shorthand, + definitionKeysAddDoc.GetDocFlag("public-key-id").Default, + definitionKeysAddDoc.GetDocFlag("public-key-id").Description, + ) + + definitionKeysRemoveDoc := man.Docs.GetCommand("policy/attributes/keys/remove", + man.WithRun(policy_DefinitionKeysRemove), + ) + definitionKeysRemoveDoc.Flags().StringP( + definitionKeysRemoveDoc.GetDocFlag("definition").Name, + definitionKeysRemoveDoc.GetDocFlag("definition").Shorthand, + definitionKeysRemoveDoc.GetDocFlag("definition").Default, + definitionKeysRemoveDoc.GetDocFlag("definition").Description, + ) + definitionKeysRemoveDoc.Flags().StringP( + definitionKeysRemoveDoc.GetDocFlag("public-key-id").Name, + definitionKeysRemoveDoc.GetDocFlag("public-key-id").Shorthand, + definitionKeysRemoveDoc.GetDocFlag("public-key-id").Default, + definitionKeysRemoveDoc.GetDocFlag("public-key-id").Description, + ) + + definitionKeysListDoc := man.Docs.GetCommand("policy/attributes/keys/list", + man.WithRun(policy_DefinitionKeysList), + ) + definitionKeysListDoc.Flags().StringP( + definitionKeysListDoc.GetDocFlag("definition").Name, + definitionKeysListDoc.GetDocFlag("definition").Shorthand, + definitionKeysListDoc.GetDocFlag("definition").Default, + definitionKeysListDoc.GetDocFlag("definition").Description, + ) + definitionKeysListDoc.Flags().BoolP( + definitionKeysListDoc.GetDocFlag("show-public-key").Name, + definitionKeysListDoc.GetDocFlag("show-public-key").Shorthand, + definitionKeysListDoc.GetDocFlag("show-public-key").DefaultAsBool(), + definitionKeysListDoc.GetDocFlag("show-public-key").Description, + ) + + policy_DefinitionKeysCmd.AddSubcommands(definitionKeysAddDoc, definitionKeysRemoveDoc, definitionKeysListDoc) + unsafeCmd.AddSubcommands(reactivateCmd, deleteCmd, unsafeUpdateCmd) - policy_attributesCmd.AddSubcommands(createDoc, getDoc, listDoc, updateDoc, deactivateDoc, unsafeCmd) + policy_attributesCmd.AddSubcommands(createDoc, getDoc, listDoc, updateDoc, deactivateDoc, unsafeCmd, policy_DefinitionKeysCmd) policyCmd.AddCommand(&policy_attributesCmd.Command) } diff --git a/docs/man/policy/attributes/keys/_index.md b/docs/man/policy/attributes/keys/_index.md new file mode 100644 index 00000000..44f5ce7d --- /dev/null +++ b/docs/man/policy/attributes/keys/_index.md @@ -0,0 +1,12 @@ +--- +title: Manage Attribute Keys +command: + name: keys + aliases: + - k + - key +--- + +The `keys` command allows management of public keys associated with an attribute. These keys are then used during the encryption operation to wrap individual key splits when creating a new TDF(Trusted Data Format). + +For more information on how key splitting work, see the [OpenTDF Documentation](https://opentdf.io/components/policy/key_access_grants). \ No newline at end of file diff --git a/docs/man/policy/attributes/keys/add.md b/docs/man/policy/attributes/keys/add.md new file mode 100644 index 00000000..2a0da01b --- /dev/null +++ b/docs/man/policy/attributes/keys/add.md @@ -0,0 +1,28 @@ +--- +title: Add a Public Key Mapping +command: + name: add + aliases: + - a + flags: + - name: public-key-id + shorthand: i + description: ID of the Public Key + required: true + - name: definition + shorthand: d + description: ID or FQN of the Attribute Definition + required: true +--- + +Add a public key mapping to an attribute definition. + +## Example + +```shell +otdfctl policy attributes definitions keys add --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --definition=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +otdfctl policy attributes definitions keys add --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --definition=https://example.com/attr/attr1 +``` diff --git a/docs/man/policy/attributes/keys/list.md b/docs/man/policy/attributes/keys/list.md new file mode 100644 index 00000000..0966a6ec --- /dev/null +++ b/docs/man/policy/attributes/keys/list.md @@ -0,0 +1,29 @@ +--- +title: List Attribute Keys +command: + name: list + aliases: + - l + flags: + - name: definition + shorthand: d + description: ID or FQN of the Attribute Definition + required: true + - name: show-public-key + description: Show the public key + default: false +--- + +List the public key mappings for an attribute definition. + +## Example + +```shell +# List public key mappings with Definition ID +otdfctl policy attributes definitions keys list --definition=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# List public key mappings with Definition FQN +otdfctl policy attributes definitions keys list --definition=https://example.com/attr/attr1 +``` \ No newline at end of file diff --git a/docs/man/policy/attributes/keys/remove.md b/docs/man/policy/attributes/keys/remove.md new file mode 100644 index 00000000..8fc35efb --- /dev/null +++ b/docs/man/policy/attributes/keys/remove.md @@ -0,0 +1,28 @@ +--- +title: Remove a Public Key Mapping +command: + name: remove + aliases: + - r + flags: + - name: public-key-id + shorthand: i + description: ID of the Public Key + required: true + - name: definition + shorthand: d + description: ID or FQN of the Attribute Definition + required: true +--- + +Remove a public key mapping from an attribute definition. + +## Example + +```shell +otdfctl policy attributes definitions keys remove --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --definition=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +otdfctl policy attributes definitions keys remove --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --definition=https://example.com/attr/attr1 +``` diff --git a/docs/man/policy/attributes/namespaces/keys/_index.md b/docs/man/policy/attributes/namespaces/keys/_index.md new file mode 100644 index 00000000..ca43b7fb --- /dev/null +++ b/docs/man/policy/attributes/namespaces/keys/_index.md @@ -0,0 +1,12 @@ +--- +title: Manage Namespace Keys +command: + name: keys + aliases: + - k + - key +--- + +The `keys` command allows management of public keys associated with an attribute namespace. These keys are then used during the encryption operation to wrap individual key splits when creating a new TDF(Trusted Data Format). + +For more information on how key splitting work, see the [OpenTDF Documentation](https://opentdf.io/components/policy/key_access_grants). diff --git a/docs/man/policy/attributes/namespaces/keys/add.md b/docs/man/policy/attributes/namespaces/keys/add.md new file mode 100644 index 00000000..84512271 --- /dev/null +++ b/docs/man/policy/attributes/namespaces/keys/add.md @@ -0,0 +1,30 @@ +--- +title: Add a Public Key Mapping +command: + name: add + aliases: + - a + flags: + - name: public-key-id + shorthand: i + description: ID of the Public Key + required: true + - name: namespace + shorthand: n + description: ID or FQN of the Attribute Namespace + required: true +--- + +Add a public key mapping to an attribute namespace. + +## Example + +```shell +# Add a public key mapping with Namespace ID +otdfctl policy attributes namespaces keys add --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --namespace=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# Add a public key mapping with Namespace FQN +otdfctl policy attributes namespaces keys add --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --namespace=https://example.namespace +``` diff --git a/docs/man/policy/attributes/namespaces/keys/list.md b/docs/man/policy/attributes/namespaces/keys/list.md new file mode 100644 index 00000000..d9dd0cc2 --- /dev/null +++ b/docs/man/policy/attributes/namespaces/keys/list.md @@ -0,0 +1,29 @@ +--- +title: List Namespace Keys +command: + name: list + aliases: + - l + flags: + - name: namespace + shorthand: n + description: ID or FQN of the Attribute Namespace + required: true + - name: show-public-key + description: Show the public key + default: false +--- + +List the public key mappings for an attribute namespace. + +## Example + +```shell +# List public key mappings with Namespace ID +otdfctl policy attributes namespaces keys list --namespace=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# List public key mappings with Namespace FQN +otdfctl policy attributes namespaces keys list --namespace=https://example.namespace +``` \ No newline at end of file diff --git a/docs/man/policy/attributes/namespaces/keys/remove.md b/docs/man/policy/attributes/namespaces/keys/remove.md new file mode 100644 index 00000000..fda1e2d5 --- /dev/null +++ b/docs/man/policy/attributes/namespaces/keys/remove.md @@ -0,0 +1,30 @@ +--- +title: Remove a Public Key Mapping +command: + name: remove + aliases: + - r + flags: + - name: public-key-id + shorthand: i + description: ID of the Public Key + required: true + - name: namespace + shorthand: d + description: ID or FQN of the Attribute Namespace + required: true +--- + +Remove a public key mapping from an attribute namespace. + +## Example + +```shell +# Remove a public key mapping with Namespace ID +otdfctl policy attributes namespaces keys remove --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --namespace=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# Remove a public key mapping with Namespace FQN +otdfctl policy attributes namespaces keys remove --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --namespace=https://example.namespace +``` diff --git a/docs/man/policy/attributes/values/keys/_index.md b/docs/man/policy/attributes/values/keys/_index.md new file mode 100644 index 00000000..10cbfef0 --- /dev/null +++ b/docs/man/policy/attributes/values/keys/_index.md @@ -0,0 +1,12 @@ +--- +title: Manage Value Keys +command: + name: keys + aliases: + - k + - key +--- + +The `keys` command allows management of public keys associated with an attribute value. These keys are then used during the encryption operation to wrap individual key splits when creating a new TDF(Trusted Data Format). + +For more information on how key splitting work, see the [OpenTDF Documentation](https://opentdf.io/components/policy/key_access_grants). \ No newline at end of file diff --git a/docs/man/policy/attributes/values/keys/add.md b/docs/man/policy/attributes/values/keys/add.md new file mode 100644 index 00000000..76fe2940 --- /dev/null +++ b/docs/man/policy/attributes/values/keys/add.md @@ -0,0 +1,28 @@ +--- +title: Add a Public Key +command: + name: add + aliases: + - a + flags: + - name: public-key-id + shorthand: i + description: ID of the Public Key + required: true + - name: value + shorthand: v + description: ID or FQN of the Attribute Value + required: true +--- + +Add a public key mapping to an attribute value. + +## Example + +```shell +otdfctl policy attributes values keys add --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --value=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +otdfctl policy attributes values keys add --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --value=https://example.com/attr/attr1/value/val1 +``` diff --git a/docs/man/policy/attributes/values/keys/list.md b/docs/man/policy/attributes/values/keys/list.md new file mode 100644 index 00000000..296b16ba --- /dev/null +++ b/docs/man/policy/attributes/values/keys/list.md @@ -0,0 +1,29 @@ +--- +title: List Key Mappings +command: + name: list + aliases: + - l + flags: + - name: value + shorthand: v + description: ID or FQN of the Attribute Value + required: true + - name: show-public-key + description: Show the public key + default: false +--- + +List the public key mappings for an attribute value. + +## Example + +```shell +# List public key mappings with Value ID +otdfctl policy attributes values keys list --value=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# List public key mappings with Value FQN +otdfctl policy attributes values keys list --value=https://example.com/attr/attr1/value/val1 +``` \ No newline at end of file diff --git a/docs/man/policy/attributes/values/keys/remove.md b/docs/man/policy/attributes/values/keys/remove.md new file mode 100644 index 00000000..55eed382 --- /dev/null +++ b/docs/man/policy/attributes/values/keys/remove.md @@ -0,0 +1,31 @@ +--- +title: Remove a Public Key +command: + name: remove + aliases: + - r + flags: + - name: public-key-id + shorthand: i + description: ID of the Public Key + required: true + - name: value + shorthand: v + description: ID or FQN of the Attribute Value + required: true +--- + +Remove a public key mapping from an attribute value. + +## Example + +```shell +# Remove a public key mapping with Value ID +otdfctl policy attributes values keys remove --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --value=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# Remove a public key mapping with Value FQN +otdfctl policy attributes values keys remove --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b --value=https://example.com/attr/attr1/value/val1 +``` + diff --git a/docs/man/policy/kas-registry/create.md b/docs/man/policy/kas-registry/create.md index 02d1735a..7f9bc82e 100644 --- a/docs/man/policy/kas-registry/create.md +++ b/docs/man/policy/kas-registry/create.md @@ -13,11 +13,10 @@ command: required: true - name: public-keys shorthand: c - description: One or more public keys saved for the KAS + description: (Deprecated) One or more public keys saved for the KAS - name: public-key-remote shorthand: r - description: Remote URI where the public key can be retrieved for the KAS - - name: label + description: (Deprecated) Remote URI where the public key can be retrieved for the KAS - name: name shorthand: n description: Optional name of the registered KAS (must be unique within policy) diff --git a/docs/man/policy/kas-registry/public-keys/_index.md b/docs/man/policy/kas-registry/public-keys/_index.md new file mode 100644 index 00000000..9bac99a8 --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/_index.md @@ -0,0 +1,10 @@ +--- +title: Manage Key Access Server Public Keys +command: + name: public-keys + aliases: + - pk + - public-key +--- + + diff --git a/docs/man/policy/kas-registry/public-keys/activate.md b/docs/man/policy/kas-registry/public-keys/activate.md new file mode 100644 index 00000000..f43f23f4 --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/activate.md @@ -0,0 +1,20 @@ +--- +title: Activate a Public Key +command: + name: activate + aliases: + - a + flags: + - name: id + shorthand: i + description: ID of the Public Key + required: true +--- + +Activate a public key. + +## Example + +```shell +otdfctl policy kas-registry public-keys activate --id=62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/docs/man/policy/kas-registry/public-keys/create.md b/docs/man/policy/kas-registry/public-keys/create.md new file mode 100644 index 00000000..fa0b7275 --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/create.md @@ -0,0 +1,51 @@ +--- +title: Add a Public Key to a Key Access Server +command: + name: create + aliases: + - add + flags: + - name: kas + shorthand: k + description: Key Access Server ID, Name or URI. + required: true + - name: key + shorthand: p + description: Public key to add to the KAS. Must be in PEM format. Can be base64 encoded or plain text. + required: true + - name: key-id + shorthand: i + description: ID of the public key. + required: true + - name: algorithm + shorthand: a + description: Algorithm of the public key. (rsa:2048, rsa:4096, ec:p256, ec:p384, ec:p521) + required: true + - name: label + description: "Optional metadata 'labels' in the format: key=value" + shorthand: l + default: "" +--- + +Add a public key to a Key Access Server. The public key must be in PEM format. It can be base64 encoded or plain text. + +If a key exists with the same algorithm already, the new key will be marked as active and the existing key will be marked as inactive. The namespace, attribute and value mappings will be updated to point to the new key. + +## Example + +```shell +# Add a public key to a Key Access Server By ID +otdfctl policy kas-registry public-key create --kas 62857b55-560c-4b67-96e3-33e4670ecb3b --key-id key-1 --key "-----BEGIN CERTIFICATE-----\nMIIB...5Q=\n-----END CERTIFICATE-----\n" --algorithm rsa:2048 +``` + +```shell +# Add a public key to a Key Access Server By Name +otdfctl policy kas-registry public-key +create --kas kas-1 --key-id key-1 --key "-----BEGIN CERTIFICATE-----\nMIIB...5Q=\n-----END CERTIFICATE-----\n" --algorithm rsa:2048 +``` + +```shell +# Add a public key to a Key Access Server By URI +otdfctl policy kas-registry public-key +create --kas https://example.com/kas --key-id key-1 --key "-----BEGIN CERTIFICATE-----\nMIIB...5Q=\n-----END CERTIFICATE-----\n" --algorithm rsa:2048 +``` diff --git a/docs/man/policy/kas-registry/public-keys/deactivate.md b/docs/man/policy/kas-registry/public-keys/deactivate.md new file mode 100644 index 00000000..17649b81 --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/deactivate.md @@ -0,0 +1,20 @@ +--- +title: Deactivate a Public Key +command: + name: deactivate + aliases: + - d + flags: + - name: id + shorthand: i + description: ID of the Public Key + required: true +--- + +Deactivate a public key. + +## Example + +```shell +otdfctl policy kas-registry public-keys deactivate --id=62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/docs/man/policy/kas-registry/public-keys/get.md b/docs/man/policy/kas-registry/public-keys/get.md new file mode 100644 index 00000000..2feffc02 --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/get.md @@ -0,0 +1,20 @@ +--- +title: Get a Public Key +command: + name: get + aliases: + - g + flags: + - name: id + shorthand: i + description: ID of the Public Key + required: true +--- + +Get a public key. + +## Example + +```shell +otdfctl policy kas-registry public-key get --id=62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/docs/man/policy/kas-registry/public-keys/list-mappings.md b/docs/man/policy/kas-registry/public-keys/list-mappings.md new file mode 100644 index 00000000..ba74aa9b --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/list-mappings.md @@ -0,0 +1,49 @@ +--- +title: List Public Key Mappings +command: + name: list-mappings + aliases: + - lm + flags: + - name: kas + shorthand: k + description: Key Access Server ID, Name or URI. + - name: public-key-id + shorthand: p + description: Public Key ID + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +List public key mappings shows a list of Key Access Servers and associated public keys. Each Key also has a list of namespaces, attribute defnitions and attribute values that are associated with it. + +## Example + +```shell +# List public key mappings +otdfctl policy kas-registry public-keys list-mappings +``` + +```shell +# List public key mappings with Key Access Server ID +otdfctl policy kas-registry public-keys list-mappings --kas=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# List public key mappings with Key Access Server Name +otdfctl policy kas-registry public-keys list-mappings --kas=example-kas +``` + +```shell +# List public key mappings with Key Access Server URI +otdfctl policy kas-registry public-keys list-mappings --kas=https://example.com/kas +``` + +```shell +# List public key mappings with Public Key ID +otdfctl policy kas-registry public-keys list-mappings --public-key-id=62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/docs/man/policy/kas-registry/public-keys/list.md b/docs/man/policy/kas-registry/public-keys/list.md new file mode 100644 index 00000000..93a5eb3a --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/list.md @@ -0,0 +1,43 @@ +--- +title: List Public Keys +command: + name: list + aliases: + - l + flags: + - name: kas + shorthand: k + description: Key Access Server ID, Name or URI. + - name: show-public-key + description: Show the public key + default: false + - name: limit + shorthand: l + description: Limit retrieved count + - name: offset + shorthand: o + description: Offset (page) quantity from start of the list +--- + +List public keys shows a list of public keys. + +## Example + +```shell +otdfctl policy kas-registry public-key list +``` + +```shell +# List public keys with Key Access Server ID +otdfctl policy kas-registry public-keys list --kas=62857b55-560c-4b67-96e3-33e4670ecb3b +``` + +```shell +# List public keys with Key Access Server Name +otdfctl policy kas-registry public-keys list --kas=example-kas +``` + +```shell +# List public keys with Key Access Server URI +otdfctl policy kas-registry public-keys list --kas=https://example.com/kas +``` \ No newline at end of file diff --git a/docs/man/policy/kas-registry/public-keys/unsafe/_index.md b/docs/man/policy/kas-registry/public-keys/unsafe/_index.md new file mode 100644 index 00000000..56e0bc1b --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/unsafe/_index.md @@ -0,0 +1,11 @@ +--- +title: Unsafe changes to Key Access Server Public Keys +command: + name: unsafe + flags: + - name: force + description: Force unsafe change without confirmation + required: false +--- + +Unsafe changes to a Public Key are potentially dangerous or more restrictive than the default behavior. Use this command with caution. diff --git a/docs/man/policy/kas-registry/public-keys/unsafe/delete.md b/docs/man/policy/kas-registry/public-keys/unsafe/delete.md new file mode 100644 index 00000000..2a1c65ad --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/unsafe/delete.md @@ -0,0 +1,25 @@ +--- +title: Delete a Key Access Server Public Key +command: + name: delete + aliases: + - d + - del + - remove + - rm + flags: + - name: id + shorthand: i + description: ID of the Key Access Server Public Key + required: true + - name: force + description: Force deletion without interactive confirmation (dangerous) +--- + +Delete a Key Access Server Public Key. + +## Example + +```shell +otdfctl policy kas-registry public-keys unsafe delete --id=62857b55-560c-4b67-96e3-33e4670ecb3b +``` diff --git a/docs/man/policy/kas-registry/public-keys/update.md b/docs/man/policy/kas-registry/public-keys/update.md new file mode 100644 index 00000000..2b580753 --- /dev/null +++ b/docs/man/policy/kas-registry/public-keys/update.md @@ -0,0 +1,28 @@ +--- +title: Update Public Key Metadata +command: + name: update + aliases: + - u + flags: + - name: id + shorthand: i + description: ID of the Public Key + required: true + - 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 metadata of a public key. The public key information itself cannot be updated. To update a public key create a new key with the updated information. + +## Example + +```shell +otdfctl policy kas-registry public-key update --id=62857b55-560c-4b67-96e3-33e4670ecb3b --label key=value +``` + diff --git a/e2e/attributes.bats b/e2e/attributes.bats index 7ad3fbc7..16b50904 100755 --- a/e2e/attributes.bats +++ b/e2e/attributes.bats @@ -1,11 +1,12 @@ #!/usr/bin/env bats +load "./helpers.bash" # Tests for attributes 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' + export HOST="${HOST:---host http://localhost:8080}" # Create the namespace to be used by other tests @@ -15,8 +16,7 @@ setup_file() { # always create a randomly named attribute setup() { - load "${BATS_LIB_PATH}/bats-support/load.bash" - load "${BATS_LIB_PATH}/bats-assert/load.bash" + setup_helper # invoke binary with credentials run_otdfctl_attr () { @@ -30,6 +30,8 @@ setup() { # always unsafely delete the created attribute teardown() { ./otdfctl $HOST $WITH_CREDS policy attributes unsafe delete --force --id "$ATTR_ID" + + cleanup_helper } teardown_file() { @@ -205,4 +207,202 @@ teardown_file() { assert_success [ "$(echo "$output" | jq -r '.values[0].value')" = "val2" ] [ "$(echo "$output" | jq -r '.values[1].value')" = "val1" ] +} + +@test "add_remove_key_to_definition" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Add the key to the attribute definition + log_info "Running ${run_otdfctl_attr} keys add --definition $ATTR_ID --public-key-id $KID" + run_otdfctl_attr keys add --definition "$ATTR_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # Check that the key was added to the attribute definition + log_info "Running ${run_otdfctl_attr} get --id $ATTR_ID" + run_otdfctl_attr get --id "$ATTR_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -r '.keys[].id' | while read -r id; do + log_debug "Checking PK ID: $id against $PUBLIC_KEY_ID" + [ "$id" = "$PUBLIC_KEY_ID" ] || fail "KAS ID does not match" + done + + # Remove the key from the attribute definition + log_info "Running ${run_otdfctl_attr} keys remove --definition $ATTR_ID --public-key-id $PUBLIC_KEY_ID" + run_otdfctl_attr keys remove --definition "$ATTR_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # Check that the key was removed from the attribute definition + log_info "Running ${run_otdfctl_attr} get --id $ATTR_ID" + run_otdfctl_attr get --id "$ATTR_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -e 'has("keys") | not' || fail "KAS ID still present" +} + +@test "list_keys_on_definition" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Add the key to the attribute definition + log_info "Running ${run_otdfctl_attr} keys add --definition $ATTR_ID --public-key-id $PUBLIC_KEY_ID" + run_otdfctl_attr keys add --definition "$ATTR_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # List the keys on the attribute definition + log_info "Running ${run_otdfctl_attr} keys list --definition $ATTR_ID" + run_otdfctl_attr keys list --definition "$ATTR_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -r '.keys[].id' | while read -r id; do + log_debug "Checking PK ID: $id against $PUBLIC_KEY_ID" + [ "$id" = "$PUBLIC_KEY_ID" ] || fail "KAS ID does not match" + done +} + +@test "add_remove_key_to_value" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Add value to the attribute definition + log_info "Running ${run_otdfctl_attr} values create --attribute-id $ATTR_ID --value val1" + run_otdfctl_attr values create --attribute-id "$ATTR_ID" --value val1 --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + VALUE_ID=$(echo "$output" | jq -r '.id') + + # Add the key to the attribute value + log_info "Running ${run_otdfctl_attr} values keys add --value $VALUE_ID --public-key-id $KID" + run_otdfctl_attr values keys add --value "$VALUE_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # Check that the key was added to the attribute value + log_info "Running ${run_otdfctl_attr} values get --id $VALUE_ID" + run_otdfctl_attr values get --id "$VALUE_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -r '.keys[].id' | while read -r id; do + log_debug "Checking PK ID: $id against $PUBLIC_KEY_ID" + [ "$id" = "$PUBLIC_KEY_ID" ] || fail "KAS ID does not match" + done + + # Remove the key from the attribute value + log_info "Running ${run_otdfctl_attr} keys remove --value $VALUE_ID --public-key-id $PUBLIC_KEY_ID" + run_otdfctl_attr values keys remove --value "$VALUE_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # Check that the key was removed from the attribute value + log_info "Running ${run_otdfctl_attr} values get --id $VALUE_ID" + run_otdfctl_attr values get --id "$VALUE_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -e 'has("keys") | not' || fail "KAS ID still present" +} + +@test "list_keys_on_value" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Add value to the attribute definition + log_info "Running ${run_otdfctl_attr} values create --attribute-id $ATTR_ID --value val1" + run_otdfctl_attr values create --attribute-id "$ATTR_ID" --value val1 --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + VALUE_ID=$(echo "$output" | jq -r '.id') + + # Add the key to the attribute value + log_info "Running ${run_otdfctl_attr} values keys add --value $VALUE_ID --public-key-id $KID" + run_otdfctl_attr values keys add --value "$VALUE_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # List the keys on the attribute value + log_info "Running ${run_otdfctl_attr} values keys list --value $VALUE_ID" + run_otdfctl_attr values keys list --value "$VALUE_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -r '.[].id' | while read -r id; do + log_debug "Checking PK ID: $id against $PUBLIC_KEY_ID" + [ "$id" = "$PUBLIC_KEY_ID" ] || fail "KAS ID does not match" + done } \ No newline at end of file diff --git a/e2e/auth.bats b/e2e/auth.bats index 486bbabc..315e2c6c 100755 --- a/e2e/auth.bats +++ b/e2e/auth.bats @@ -5,7 +5,7 @@ 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' + export HOST="${HOST:---host http://localhost:8080}" } setup() { diff --git a/e2e/encrypt-decrypt.bats b/e2e/encrypt-decrypt.bats index 9861b593..3bc069ad 100755 --- a/e2e/encrypt-decrypt.bats +++ b/e2e/encrypt-decrypt.bats @@ -7,7 +7,7 @@ setup_file() { echo -n '{"clientId":"opentdf","clientSecret":"secret"}' > $CREDSFILE export WITH_CREDS="--with-client-creds-file $CREDSFILE" export DEBUG_LEVEL="--log-level debug" - export HOST=http://localhost:8080 + export HOST="${HOST:---host http://localhost:8080}" export INFILE_GO_MOD=go.mod export OUTFILE_GO_MOD=go.mod.tdf diff --git a/e2e/helpers.bash b/e2e/helpers.bash new file mode 100644 index 00000000..a0e913e9 --- /dev/null +++ b/e2e/helpers.bash @@ -0,0 +1,211 @@ +# helpers.bash +#!/usr/bin/env bash + +# OTDFCTL Helper Functions +run_otdfctl_kasr() { + run sh -c "./otdfctl policy kas-registry $HOST $WITH_CREDS $*" +} + +create_kas() { + log_debug "Creating KAS... $1 $2" + + run_otdfctl_kasr create --uri "$1" -n "$2" --json + + log_debug "Created KAS: $output" # Debug log: the output of the create command + + KAS_ID=$(echo "$output" | jq -r '.id') +} + +create_public_key() { + local kas="$1" + local key_id="$2" + local algorithm="$3" + local key_content + local label_args="$4" + + log_debug "Creating public key..." + + # Select the appropriate key generation function based on the algorithm + case "$algorithm" in + "$RSA_2048_ALG") + eval "$(gen_rsa_2048)" + key_content="$RSA_2048_PUBLIC_KEY" + ;; + "$RSA_4096_ALG") + eval "$(gen_rsa_4096)" + key_content="$RSA_4096_PUBLIC_KEY" + ;; + "$EC_256_ALG") + eval "$(gen_ec256)" + key_content="$EC_256_PUBLIC_KEY" + ;; + "$EC_384_ALG") + eval "$(gen_ec384)" + key_content="$EC_384_PUBLIC_KEY" + ;; + "$EC_521_ALG") + eval "$(gen_ec521)" + key_content="$EC_521_PUBLIC_KEY" + ;; + *) + log_error "Unsupported algorithm: $algorithm" + return 1 + ;; + esac + + # Verify key content is not empty + if [ -z "$key_content" ]; then + log_info "Empty key content for algorithm: $algorithm" + return 1 + fi + + # Base64 encode the key content + key_content=$(echo "$key_content" | base64 -w 0) + + log_debug "Running ${run_otdfctl_kasr} public-key create --kas $kas --key "$key_content" --key-id $key_id --algorithm $algorithm $label_args --json" + run_otdfctl_kasr public-key create \ + --kas "$kas" \ + --key "$key_content" \ + --key-id "$key_id" \ + --algorithm "$algorithm" \ + $label_args \ + --json + + if [ -z "$output" ]; then + log_info "Failed to create public key" + return 1 + fi + + log_debug "Created public key: $output" + + PUBLIC_KEY_ID=$(echo "$output" | jq -r '.id') + PUBLIC_KEY_IDS+=("$PUBLIC_KEY_ID") +} + +# Setup Helper +setup_helper() { + load "${BATS_LIB_PATH}/bats-support/load.bash" + load "${BATS_LIB_PATH}/bats-assert/load.bash" + + # Initialize IDs to empty strings in case creation fails + KAS_ID="" + PUBLIC_KEY_ID="" + PUBLIC_KEY_IDS=() # Initialize an empty array + + KAS_URI="https://testing-public-key.io" + KAS_NAME="public-key-kas" + + RSA_2048_ALG="rsa:2048" + RSA_4096_ALG="rsa:4096" + EC_256_ALG="ec:secp256r1" + EC_384_ALG="ec:secp384r1" + EC_521_ALG="ec:secp521r1" +} + +# Cleanup Helper +cleanup_helper() { + # Iterate over the array of public key IDs and delete them + for PUBLIC_KEY_ID in "${PUBLIC_KEY_IDS[@]}"; do + if [ -n "$PUBLIC_KEY_ID" ]; then + log_debug "Running ${run_otdfctl_kasr} public-key unsafe delete --id $PUBLIC_KEY_ID --force --json" + run_otdfctl_kasr public-key unsafe delete --id "$PUBLIC_KEY_ID" --force --json + log_debug "$output" + if [ $? -ne 0 ]; then + log_info "Error: Failed to delete public key with ID: $PUBLIC_KEY_ID" + fi + log_debug "Deleted public key with ID: $PUBLIC_KEY_ID" + fi + done + if [ -n "$KAS_ID" ]; then + log_debug "Running ${run_otdfctl_kasr} delete --id $KAS_ID --force --json" + run_otdfctl_kasr delete --id "$KAS_ID" --force --json + log_debug "$output" + if [ $? -ne 0 ]; then + log_info "Error: Failed to delete KAS registry with ID: $KAS_ID" + fi + log_debug "Deleted KAS registry with ID: $KAS_ID" + fi +} + +# Helper function for debug logging +log_debug() { + if [[ "${BATS_DEBUG:-0}" == "1" ]]; then + echo "DEBUG($BATS_TEST_NAME): $1" >&3 + fi +} + +# Helper function for info logging +log_info() { + echo "INFO($BATS_TEST_NAME): $1" >&3 +} + +# Helper function to generate a rsa 2048 key pair +gen_rsa_2048() { + log_debug "Generating RSA 2048 key pair" + local private_key public_key + + # Generate private key + private_key=$(openssl genrsa 2048) + + # Extract public key + public_key=$(echo "$private_key" | openssl rsa -pubout) + + # Output using proper escaping + printf 'export RSA_2048_PUBLIC_KEY=%q\n' "$public_key" +} + +# Helper function to generate a rsa 4096 key pair +gen_rsa_4096() { + log_debug "Generating RSA 4096 key pair" + local private_key public_key + + # Generate private key + private_key=$(openssl genrsa 4096) + + # Extract public key + public_key=$(echo "$private_key" | openssl rsa -pubout) + + printf 'export RSA_4096_PUBLIC_KEY=%q\n' "$public_key" +} + +# Helper function to generate an EC 256 key pair +gen_ec256() { + log_debug "Generating EC 256 key pair" + local private_key public_key + + # Generate private key + private_key=$(openssl ecparam -name prime256v1 -genkey) + + # Extract public key + public_key=$(echo "$private_key" | openssl ec -pubout) + + printf 'export EC_256_PUBLIC_KEY=%q\n' "$public_key" +} + +# Helper function to generate an EC 384 key pair +gen_ec384() { + log_debug "Generating EC 384 key pair" + local private_key public_key + + # Generate private key + private_key=$(openssl ecparam -name secp384r1 -genkey) + + # Extract public key + public_key=$(echo "$private_key" | openssl ec -pubout) + + printf 'export EC_384_PUBLIC_KEY=%q\n' "$public_key" +} + +# Helper function to generate an EC 521 key pair +gen_ec521() { + log_debug "Generating EC 521 key pair" + local private_key public_key + + # Generate private key + private_key=$(openssl ecparam -name secp521r1 -genkey) + + # Extract public key + public_key=$(echo "$private_key" | openssl ec -pubout) + + printf 'export EC_521_PUBLIC_KEY=%q\n' "$public_key" +} diff --git a/e2e/kas-grants.bats b/e2e/kas-grants.bats index d8646f72..91a47c39 100755 --- a/e2e/kas-grants.bats +++ b/e2e/kas-grants.bats @@ -5,7 +5,7 @@ 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' + export HOST="${HOST:---host http://localhost:8080}" export KAS_URI="https://e2etestkas.com" export KAS_ID=$(./otdfctl $HOST $WITH_CREDS policy kas-registry create --uri "$KAS_URI" --public-key-remote 'https://e2etestkas.com/pub_key' --json | jq -r '.id') diff --git a/e2e/kas-registry.bats b/e2e/kas-registry.bats index 75d2f6a5..461cc6f9 100755 --- a/e2e/kas-registry.bats +++ b/e2e/kas-registry.bats @@ -1,4 +1,5 @@ #!/usr/bin/env bats +load "./helpers.bash" # Tests for kas registry @@ -6,28 +7,27 @@ setup_file() { export CREDSFILE=creds.json echo -n '{"clientId":"opentdf","clientSecret":"secret"}' > $CREDSFILE export WITH_CREDS="--with-client-creds-file $CREDSFILE" - export HOST='--host http://localhost:8080' + export HOST="${HOST:=--host=http://localhost:8080}" export DEBUG_LEVEL="--log-level debug" export REMOTE_KEY='https://hello.world/pubkey' PEM='-----BEGIN CERTIFICATE-----\nMIIC/TCCAeWgAwIBAgIUMu8o8Wh2HTA6TAeLCjC2f\n9pIeIwDQYJKoZIhvcNAQEL\nBQAwDjEMMAoGA1UEAwwDa2FzMB4XDTI0MDYxODE4M\nYyN1oXDTI1MDYxODE4MzYy\nN1owDjEMMAoGA1UEAwwDa2FzMIIBIjANBgkqhkiG9\n0BAQEFAAOCAQ8AMIIBCgKC\nAQEAr1pQjo7piOvPCTtdIENfG8yVi+WV1FUN/6xTD\nrLxZTtAkZ143uHTfP9a1uq\nhW1IoayJOUjnYsnQHzuEBdkZ4Huwzdy6wRneOTRcj\nN+DwnZKmDq1uafzlGsto/B\nhftmilUF4YnnFcDN+vqj2ep3abUkjhkmIQT8pr25b\nxLaiwwOnlyM5VQc8nahgln\n0M0gNWKIWFEJwhj0Zojh1L4djmzqUiOmNHBP4QzSp\n+0+tWoxIoP2OajkJy0IcZH\nq/N9iSzVbg1K/kKg+du/PmdjP+j56lkJOSRzezh+d\n7+GhrBT3UsmPncV3cWVMi8\nEsYCKcT5EMHhaNaG0XDjJmG28wIDAQABo1MwUTAdB\nNVHQ4EFgQUgPTNFczd9j0E\nX37p6HhwPRicBj8wHwYDVR0jBBgwFoAUgPTNFczd9\n0EX37p6HhwPRicBj8wDwYD\nVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCA\nEACKeqFK0JW2a5sKbOBywZ\nik0y2jrDrZPnf0odN5Hm8meenBxmyoByVVFonPeCh\nnYFStDm2QIQ6gYPmtAaCuJ\ntUyNs6LOBmpGbJhTg5yceqWZxXcsfVFwdqqUt66tW\ncOxVTBgk7xzDQOnLgFLjd6\nJVHxMzFLWTQ0kM2UrN8gtOdLk4aeBaK7bTwZPFtFt\naFebQTm4KcfR5zsfLS+8iF\nu1fF9ZJZH6g6blCTxNtwvvyS1U3/KP0VT9YPw95fp\nV2SKOd3z3Y0dJ9A9Ld9MI3\nL/Y/+5m94FB17SIkDEzY3gvNLCIVq88vXyg+ghTHs\nscc3VqE0+Lzrfdzimo31Ed\nNA==\n-----END CERTIFICATE-----' export KID='my_key_123' export CACHED_KEY=$(printf '{"cached":{"keys":[{"kid":"%s","alg":1,"pem":"%s"}]}}' "$KID" "$PEM" ) + + export FIXTURE_PUBLIC_KEY_ID="f478f1cd-df6e-4a55-9603-d961b36ea392" } setup() { - load "${BATS_LIB_PATH}/bats-support/load.bash" - load "${BATS_LIB_PATH}/bats-assert/load.bash" - - # invoke binary with credentials - run_otdfctl_kasr () { - run sh -c "./otdfctl policy kas-registry $HOST $WITH_CREDS $*" - } + setup_helper } teardown() { + log_debug "Running teardown for: $BATS_TEST_NAME" ID=$(echo "$CREATED" | jq -r '.id') run_otdfctl_kasr delete --id "$ID" --force + + cleanup_helper } @test "create registration of a KAS with remote key" { @@ -205,3 +205,575 @@ teardown() { assert_output --partial "Total" assert_line --regexp "Current Offset.*0" } + +@test "create_public_key_success" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + # # Generate the key pair and set variables + # create_ + # log_debug "RSA_2048_PUBLIC_KEY=\"$RSA_2048_PUBLIC_KEY\"" + # eval "$(gen_rsa_4096)" + # log_debug "RSA_4096_PUBLIC_KEY=\"$RSA_4096_PUBLIC_KEY\"" + # eval "$(gen_ec256)" + # log_debug "EC_256_PUBLIC_KEY=\"$EC_256_PUBLIC_KEY\"" + # eval "$(gen_ec384)" + # log_debug "EC_384_PUBLIC_KEY=\"$EC_384_PUBLIC_KEY\"" + # eval "$(gen_ec521)" + # log_debug "EC_521_PUBLIC_KEY=\"$EC_521_PUBLIC_KEY\"" + + KID="test_key_123" + + ########## Creating RSA 2048 public keys ########## + create_public_key "$KAS_ID" "$KID" "$RSA_2048_ALG" + # log_debug "Running ${run_otdfctl_kasr} public-key create --kas $KAS_ID --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$RSA_2048_ALG" --json" + # run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$RSA_2048_ALG" --json + + # log_debug "Raw Output:" # Debug log: Raw output + # log_debug "$output" + + assert_success # Check if the command ran successfully + + # Get the ID of the public key + # PUBLIC_KEY_ID=$(echo "$output" | jq -r '.id') + # PUBLIC_KEY_IDS+=("$PUBLIC_KEY_ID") + + ########## Creating RSA 4096 public keys ########## + create_public_key "$KAS_ID" "$KID" "$RSA_4096_ALG" + # log_debug "Running ${run_otdfctl_kasr} public-key create --kas $KAS_ID --key \"$(echo "$RSA_4096_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$RSA_4096_ALG" --json" + # run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$RSA_4096_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$RSA_4096_ALG" --json + + # log_debug "Raw Output:" # Debug log: Raw output + # log_debug "$output" + + assert_success # Check if the command ran successfully + + # Get the ID of the public key + # PUBLIC_KEY_ID=$(echo "$output" | jq -r '.id') + # PUBLIC_KEY_IDS+=("$PUBLIC_KEY_ID") + + ########## Creating EC 256 public keys ########## + create_public_key "$KAS_ID" "$KID" "$EC_256_ALG" + # log_debug "Running ${run_otdfctl_kasr} public-key create --kas $KAS_ID --key \"$(echo "$EC_256_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$EC_256_ALG" --json" + # run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$EC_256_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$EC_256_ALG" --json + + # log_debug "Raw Output:" # Debug log: Raw output + # log_debug "$output" + + assert_success # Check if the command ran successfully + + # Get the ID of the public key + # PUBLIC_KEY_ID=$(echo "$output" | jq -r '.id') + # PUBLIC_KEY_IDS+=("$PUBLIC_KEY_ID") + + ########## Creating EC 384 public keys ########## + create_public_key "$KAS_ID" "$KID" "$EC_384_ALG" + # log_debug "Running ${run_otdfctl_kasr} public-key create --kas $KAS_ID --key \"$(echo "$EC_384_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$EC_384_ALG" --json" + # run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$EC_384_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$EC_384_ALG" --json + + # log_debug "Raw Output:" # Debug log: Raw output + # log_debug "$output" + + assert_success # Check if the command ran successfully + + # Get the ID of the public key + # PUBLIC_KEY_ID=$(echo "$output" | jq -r '.id') + # PUBLIC_KEY_IDS+=("$PUBLIC_KEY_ID") +} + +@test "create_public_key_required_flags" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + # Generate the key pair and set variables + eval "$(gen_rsa_2048)" + log_debug "RSA_2048_PUBLIC_KEY=\"$RSA_2048_PUBLIC_KEY\"" + + ALG="rsa:2048" + KID="test_key_123" + + # Missing KAS Flag + run_otdfctl_kasr public-key create --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$ALG" --json + assert_failure # Check if the command failed requiring the KAS flag + assert_output --partial "Flag '--kas' is required" + + # Missing Key Flag + run_otdfctl_kasr public-key create --kas "$KAS_ID" --key-id "$KID" --algorithm "$ALG" --json + assert_failure # Check if the command failed requiring the Key flag + assert_output --partial "Flag '--key' is required" + + # Missing Key ID Flag + run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --algorithm "$ALG" --json + assert_failure # Check if the command failed requiring the Key ID flag + assert_output --partial "Flag '--key-id' is required" + + # Missing Algorithm Flag + run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --json + assert_failure # Check if the command failed requiring the Algorithm flag + assert_output --partial "Flag '--algorithm' is required" +} + +@test "create_public_key_invalid_algorithm" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + # Generate the key pair and set + eval "$(gen_rsa_2048)" + log_debug "RSA_2048_PUBLIC_KEY=\"$RSA_2048_PUBLIC_KEY\"" + + ALG="rsa:2048" + KID="test_key_123" + + # Invalid Algorithm + run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "rsa:4096" --json + assert_failure # Check if the command failed with an invalid algorithm + assert_output --partial "invalid rsa key size" + + # Invalid Algorithm + run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "ec:secp256r1" --json + assert_failure # Check if the command failed with an invalid algorithm + assert_output --partial "ey algorithm does not match the provided algorithm" + + # Unsupported Algorithm + run_otdfctl_kasr public-key create --kas "$KAS_ID" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "rsa:1024" --json + assert_failure # Check if the command failed with an unsupported algorithm + assert_output --partial "unsupported algorithm" +} + +@test "add_public_key_by_kas_uri" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + # Generate the key pair and set variables + # eval "$(gen_rsa_2048)" + # log_debug "PUBLIC_KEY=\"$RSA_2048_PUBLIC_KEY\"" + + ALG="rsa:2048" + KID="test_key_123" + + create_public_key "$KAS_URI" "$KID" "$ALG" + # log_debug "Running ${run_otdfctl_kasr} public-key create --kas $KAS_ID --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$RSA_2048_ALG" --json" + # run_otdfctl_kasr public-key create --kas "$KAS_URI" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$RSA_2048_ALG" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + +} + +@test "add_public_key_by_kas_name" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + # Generate the key pair and set variables + # eval "$(gen_rsa_2048)" + # log_debug "PUBLIC_KEY=\"$RSA_2048_PUBLIC_KEY\"" + + ALG="rsa:2048" + KID="test_key_123" + + create_public_key "$KAS_NAME" "$KID" "$ALG" + # log_debug "Running ${run_otdfctl_kasr} public-key create --kas "$KAS_NAME" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "rsa:2048" --json" + # run_otdfctl_kasr public-key create --kas "$KAS_NAME" --key \"$(echo "$RSA_2048_PUBLIC_KEY" | base64)\" --key-id "$KID" --algorithm "$ALG" --json + + # log_debug "Raw Output:" # Debug log: Raw output + # log_debug "$output" + + assert_success # Check if the command ran successfully +} + +@test "update_public_key_labels" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Update the public key with labels + log_debug "Running ${run_otdfctl_kasr} public-key update --id $PUBLIC_KEY_ID --label test=test --json" + run_otdfctl_kasr public-key update --id "$PUBLIC_KEY_ID" --label test=test --json + + log_debug "Raw Output:" # Debug log: Raw output + + assert_success # Check if the command ran successfully + + # Get public key by ID and check if the labels are set + log_debug "Running ${run_otdfctl_kasr} public-key get --id $PUBLIC_KEY_ID --json" + run_otdfctl_kasr public-key get --id "$PUBLIC_KEY_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + + assert_success # Check if the command ran successfully + + # Check json response for the labels + echo "$output" | jq -e '.metadata.labels | has("test")' || fail "Label not found" +} + +@test "update_public_key_labels_force" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" "--label test=test" + + # Update the public key with labels + log_debug "Running ${run_otdfctl_kasr} public-key update --id $PUBLIC_KEY_ID --label test1=test1 --force-replace-labels --json" + run_otdfctl_kasr public-key update --id "$PUBLIC_KEY_ID" --label test1=test1 --force-replace-labels --json + + log_debug "Raw Output:" # Debug log: Raw output + + assert_success # Check if the command ran successfully + + # Get public key by ID and check if the labels are set + log_debug "Running ${run_otdfctl_kasr} public-key get --id $PUBLIC_KEY_ID --json" + run_otdfctl_kasr public-key get --id "$PUBLIC_KEY_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + + assert_success # Check if the command ran successfully + + # Check json response for the labels + echo "$output" | jq -e '.metadata.labels | (has("test") | not) and has("test1")' || fail "Labels check failed" +} + +@test "get_public_key" { + log_info "Starting test: $BATS_TEST_NAME" + + log_debug "Running ${run_otdfctl_kasr} public-key get --id $FIXTURE_PUBLIC_KEY_ID --json" + run_otdfctl_kasr public-key get --id $FIXTURE_PUBLIC_KEY_ID --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + if ! echo "$output" | jq -e ; then + fail "Output is not valid JSON" + fi + + # Parse the JSON output using jq + output_json=$(echo "$output" | jq -c '.') + + echo "$output_json" | jq -e ' + .[0] as $root | + [ + if $root.id then empty else "id" end, + if $root.is_active then empty else "is_active" end, + if $root.was_mapped then empty else "was_mapped" end, + if $root.public_key then empty else "public_key" end, + if $root.public_key then ( + if $root.public_key.pem then empty else "public_key.pem" end, + if $root.public_key.kid then empty else "public_key.kid" end, + if $root.public_key.alg then empty else "public_key.alg" end + ) else empty end, + if $root.kas then empty else "kas" end, + if $root.kas then ( + if $root.kas.id then empty else "kas.id" end, + if $root.kas.uri then empty else "kas.uri" end, + if $root.kas.name then empty else "kas.name" end + ) else empty end, + if $root.metadata then empty else "metadata" end + ] | if length > 0 then error("Missing fields: " + join(", ")) else true end +' || fail "Structure validation failed" +} + +@test "get_public_key_required_flags" { + log_info "Starting test: $BATS_TEST_NAME" + # Missing ID Flag + run_otdfctl_kasr public-key get --json + assert_failure # Check if the command failed requiring the ID flag + assert_output --partial "Flag '--id' is required" +} + + +@test "list_public_keys" { + log_info "Starting test: $BATS_TEST_NAME" + + log_debug "Running ${run_otdfctl_kasr} public-key list --json" + run_otdfctl_kasr public-key list --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Parse the JSON output using jq + output_json=$(echo "$output" | jq -c '.') + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output_json" | jq 'length')" -gt 0 ] + + echo "$output_json" | jq -e ' + .[0] as $root | + [ + if $root.id then empty else "id" end, + if $root.is_active then empty else "is_active" end, + if $root.was_mapped then empty else "was_mapped" end, + if $root.public_key then empty else "public_key" end, + if $root.public_key then ( + if $root.public_key.pem then empty else "public_key.pem" end, + if $root.public_key.kid then empty else "public_key.kid" end, + if $root.public_key.alg then empty else "public_key.alg" end + ) else empty end, + if $root.kas then empty else "kas" end, + if $root.kas then ( + if $root.kas.id then empty else "kas.id" end, + if $root.kas.uri then empty else "kas.uri" end, + if $root.kas.name then empty else "kas.name" end + ) else empty end, + if $root.metadata then empty else "metadata" end + ] | if length > 0 then error("Missing fields: " + join(", ")) else true end +' || fail "Structure validation failed" +} + +@test "list_public_keys_by_kas" { + # Create a KAS to Filter By + ALG="rsa:2048" + + create_kas "$KAS_URI" "$KAS_NAME" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Filter By ID + log_debug "Running ${run_otdfctl_kasr} public-key list --kas "$KAS_ID" --json" + run_otdfctl_kasr public-key list --kas "$KAS_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output" | jq 'length')" -gt 0 ] + + echo "$output" | jq -r '.[].kas.id' | while read -r id; do + log_debug "Checking KAS ID: $id against $KAS_ID" + [ "$id" = "$KAS_ID" ] || fail "KAS ID does not match" + done + + + # Filter By URI + log_debug "Running ${run_otdfctl_kasr} public-key list --kas "$KAS_URI" --json" + run_otdfctl_kasr public-key list --kas "$KAS_URI" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output" | jq 'length')" -gt 0 ] + + echo "$output" | jq -r '.[].kas.uri' | while read -r uri; do + log_debug "Checking KAS ID: $uri against $KAS_URI" + [ "$uri" = "$KAS_URI" ] || fail "KAS ID does not match" + done + + + # Filter By Name + log_debug "Running ${run_otdfctl_kasr} public-key list --kas "$KAS_NAME" --json" + run_otdfctl_kasr public-key list --kas "$KAS_NAME" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output" | jq 'length')" -gt 0 ] + + echo "$output" | jq -r '.[].kas.name' | while read -r name; do + log_debug "Checking KAS ID: $name against $KAS_NAME" + [ "$name" = "$KAS_NAME" ] || fail "KAS ID does not match" + done +} + +@test "list_public_key_mappings" { + log_info "Starting test: $BATS_TEST_NAME" + + log_debug "Running ${run_otdfctl_kasr} public-key list-mappings --json" + run_otdfctl_kasr public-key list-mappings --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output" | jq 'length')" -gt 0 ] + + echo "$output" | jq -e ' + .[0].public_keys[] | ( + (.key | has("id")) and + (.key | has("is_active")) and + (.key | has("was_mapped")) and + (.key | has("public_key")) and + (.key.public_key | has("pem") and has("kid") and has("alg")) + ) or error("Missing required public key fields in response structure") + ' || fail "Structure validation failed" +} + +@test "list_public_key_mappings_by_kas" { + log_info "Starting test: $BATS_TEST_NAME" + + # Create a KAS to Filter By + ALG="rsa:2048" + + create_kas "$KAS_URI" "$KAS_NAME" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Filter By ID + log_debug "Running ${run_otdfctl_kasr} public-key list-mappings --kas "$KAS_ID" --json" + run_otdfctl_kasr public-key list-mappings --kas "$KAS_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output" | jq 'length')" -gt 0 ] + + echo "$output" | jq -r '.[].kas_id' | while read -r id; do + log_debug "Checking KAS ID: $id against $KAS_ID" + [ "$id" = "$KAS_ID" ] || fail "KAS ID does not match" + done + + # Filter By URI + log_debug "Running ${run_otdfctl_kasr} public-key list-mappings --kas "$KAS_URI" --json" + run_otdfctl_kasr public-key list-mappings --kas "$KAS_URI" --json + + log_debug "Raw Output:" # Debug log: Raw output + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output" | jq 'length')" -gt 0 ] + + echo "$output" | jq -r '.[].kas_uri' | while read -r uri; do + log_debug "Checking KAS ID: $uri against $KAS_URI" + [ "$uri" = "$KAS_URI" ] || fail "KAS ID does not match" + done + + + # Filter By Name + log_debug "Running ${run_otdfctl_kasr} public-key list-mappings --kas "$KAS_NAME" --json" + run_otdfctl_kasr public-key list-mappings --kas "$KAS_NAME" --json + + log_debug "Raw Output:" # Debug log: Raw output + + assert_success # Check if the command ran successfully + + # Check if the output is valid JSON and is an array (without using jq -t) + if ! echo "$output" | jq -e '.[0]'; then + fail "Output is not a JSON array" + fi + + # Check if the output is not empty (contains at least one key) + [ "$(echo "$output" | jq 'length')" -gt 0 ] + + echo "$output" | jq -r '.[].kas_name' | while read -r name; do + log_debug "Checking KAS ID: $name against $KAS_NAME" + [ "$name" = "$KAS_NAME" ] || fail "KAS ID does not match" + done +} + +@test "activate_deactivate_public_key" { + log_info "Starting test: $BATS_TEST_NAME" + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Deactivate the public key + log_debug "Running ${run_otdfctl_kasr} public-key deactivate --id $PUBLIC_KEY_ID --json" + run_otdfctl_kasr public-key deactivate --id "$PUBLIC_KEY_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Get public key by ID and check if the key is deactivated + log_debug "Running ${run_otdfctl_kasr} public-key get --id $PUBLIC_KEY_ID --json" + run_otdfctl_kasr public-key get --id "$PUBLIC_KEY_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Check json response for the is_active flag + echo "$output" | jq -e '.is_active == {}' || fail "Public key is still active" + + # Activate the public key + log_debug "Running ${run_otdfctl_kasr} public-key activate --id $PUBLIC_KEY_ID --json" + run_otdfctl_kasr public-key activate --id "$PUBLIC_KEY_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Get public key by ID and check if the key is activated + log_debug "Running ${run_otdfctl_kasr} public-key get --id $PUBLIC_KEY_ID --json" + run_otdfctl_kasr public-key get --id "$PUBLIC_KEY_ID" --json + + log_debug "Raw Output:" # Debug log: Raw output + log_debug "$output" + + assert_success # Check if the command ran successfully + + # Check json response for the is_active flag + echo "$output" | jq -e '.is_active.value' || fail "Public key is not active" +} \ No newline at end of file diff --git a/e2e/namespaces.bats b/e2e/namespaces.bats index dcb0ee99..b68f2e8f 100755 --- a/e2e/namespaces.bats +++ b/e2e/namespaces.bats @@ -1,11 +1,12 @@ #!/usr/bin/env bats +load "./helpers.bash" # Tests for namespaces 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' + export HOST="${HOST:---host http://localhost:8080}" # Create the namespace to be used by other tests @@ -16,8 +17,7 @@ setup_file() { } setup() { - load "${BATS_LIB_PATH}/bats-support/load.bash" - load "${BATS_LIB_PATH}/bats-assert/load.bash" + setup_helper # invoke binary with credentials run_otdfctl_ns () { @@ -25,6 +25,13 @@ setup() { } } +teardown() { + cleanup_helper + + # cleanup + run_otdfctl_ns unsafe delete --id $NS_ID --force +} + teardown_file() { # clear out all test env vars unset HOST WITH_CREDS NS_NAME NS_FQN NS_ID NS_ID_FLAG @@ -190,3 +197,111 @@ teardown_file() { run_otdfctl_ns list --state active echo $output | refute_output --partial "$NS_ID" } + +@test "add_remove_key_to_namespace" { + log_info "Starting test: $BATS_TEST_NAME" + + run_otdfctl_ns create --name keys.test --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + NAMESPACE_ID=$(echo "$output" | jq -r '.id') + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Add the key to the attribute namespace + log_info "Running ${run_otdfctl_ns} keys add --value $NAMESPACE_ID --public-key-id $KID" + run_otdfctl_ns keys add --namespace "$NAMESPACE_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # Check that the key was added to the attribute namespace + log_info "Running ${run_otdfctl_ns} get --id $NAMESPACE_ID" + run_otdfctl_ns get --id "$NAMESPACE_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -r '.keys[].id' | while read -r id; do + log_debug "Checking PK ID: $id against $PUBLIC_KEY_ID" + [ "$id" = "$PUBLIC_KEY_ID" ] || fail "KAS ID does not match" + done + + # Remove the key from the attribute namespaces + log_info "Running ${run_otdfctl_ns} keys remove --namespace $NAMESPACE_ID --public-key-id $PUBLIC_KEY_ID" + run_otdfctl_ns keys remove --namespace "$NAMESPACE_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # Check that the key was removed from the attribute namespaces + log_info "Running ${run_otdfctl_ns} get --id $NAMESPACE_ID" + run_otdfctl_ns get --id "$NAMESPACE_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -e 'has("keys") | not' || fail "KAS ID still present" + + run_otdfctl_ns unsafe delete --id $NAMESPACE_ID --force +} + +@test "list_keys_on_namesapces" { + log_info "Starting test: $BATS_TEST_NAME" + + run_otdfctl_ns create --name keys.test --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + NAMESPACE_ID=$(echo "$output" | jq -r '.id') + + create_kas "$KAS_URI" "$KAS_NAME" + + ALG="rsa:2048" + KID="test" + + create_public_key "$KAS_ID" "$KID" "$ALG" + + # Add the key to the attribute namespace + log_info "Running ${run_otdfctl_ns} keys add --value $NAMESPACE_ID --public-key-id $KID" + run_otdfctl_ns keys add --namespace "$NAMESPACE_ID" --public-key-id "$PUBLIC_KEY_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + # Check that the key was added to the attribute namespace + log_info "Running ${run_otdfctl_ns} get --id $NAMESPACE_ID" + run_otdfctl_ns get --id "$NAMESPACE_ID" --json + + log_debug "Raw output:" + log_debug "$output" + + assert_success + + echo "$output" | jq -r '.keys[].id' | while read -r id; do + log_debug "Checking PK ID: $id against $PUBLIC_KEY_ID" + [ "$id" = "$PUBLIC_KEY_ID" ] || fail "KAS ID does not match" + done +} \ No newline at end of file diff --git a/e2e/resource-mapping.bats b/e2e/resource-mapping.bats index b637602b..8fd70ec5 100755 --- a/e2e/resource-mapping.bats +++ b/e2e/resource-mapping.bats @@ -5,7 +5,7 @@ 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' + export HOST="${HOST:---host http://localhost:8080}" # Create two namespaced values to be used in other tests NS_NAME="resource-mappings.io" diff --git a/e2e/subject-condition-sets.bats b/e2e/subject-condition-sets.bats index 283d2f5e..cad6ef38 100755 --- a/e2e/subject-condition-sets.bats +++ b/e2e/subject-condition-sets.bats @@ -5,7 +5,7 @@ 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' + export HOST="${HOST:---host http://localhost:8080}" export SCS_1='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["marketing"],"subject_external_selector_value":".org.name"},{"operator":1,"subject_external_values":["ShinyThing"],"subject_external_selector_value":".team.name"}],"boolean_operator":1}]}]' export SCS_2='[{"condition_groups":[{"conditions":[{"operator":3,"subject_external_values":["piedpiper.com","hooli.com"],"subject_external_selector_value":".emailAddress"},{"operator":1,"subject_external_values":["sales"],"subject_external_selector_value":".department"}],"boolean_operator":2}]}]' diff --git a/e2e/subject-mapping.bats b/e2e/subject-mapping.bats index cfb7b437..bd1a41b9 100755 --- a/e2e/subject-mapping.bats +++ b/e2e/subject-mapping.bats @@ -5,7 +5,7 @@ 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' + export HOST="${HOST:---host http://localhost:8080}" # Create two namespaced values to be used in other tests NS_NAME="subject-mappings.net" diff --git a/go.mod b/go.mod index 6f0c1b7d..40a34533 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/glamour v0.8.0 github.com/charmbracelet/huh v0.5.2 github.com/charmbracelet/lipgloss v1.0.0 + github.com/charmbracelet/x/term v0.2.1 github.com/creasty/defaults v1.8.0 github.com/evertras/bubble-table v0.17.1 github.com/gabriel-vasile/mimetype v1.4.8 @@ -16,7 +17,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 github.com/opentdf/platform/lib/flattening v0.1.3 - github.com/opentdf/platform/protocol/go v0.2.22 + github.com/opentdf/platform/protocol/go v0.2.26 github.com/opentdf/platform/sdk v0.3.25 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 @@ -40,7 +41,6 @@ require ( github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.4.5 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect diff --git a/go.sum b/go.sum index 37e8a9f5..7ff542e0 100644 --- a/go.sum +++ b/go.sum @@ -228,8 +228,8 @@ github.com/opentdf/platform/lib/flattening v0.1.3 h1:IuOm/wJVXNrzOV676Ticgr0wyBk github.com/opentdf/platform/lib/flattening v0.1.3/go.mod h1:Gs/T+6FGZKk9OAdz2Jf1R8CTGeNRYrq1lZGDeYT3hrY= github.com/opentdf/platform/lib/ocrypto v0.1.7 h1:IcCYRrwmMqntqUE8frmUDg5EZ0WMdldpGeGhbv9+/A8= github.com/opentdf/platform/lib/ocrypto v0.1.7/go.mod h1:4bhKPbRFzURMerH5Vr/LlszHvcoXQbfJXa0bpY7/7yg= -github.com/opentdf/platform/protocol/go v0.2.22 h1:C/jjtwu5yTon8g0ewuN29QE7VXSQHyb2dx9W0U6Oqok= -github.com/opentdf/platform/protocol/go v0.2.22/go.mod h1:skpOCVuWSjUHazLKOkh3nSB057OB4sHICe7MpmJY9KU= +github.com/opentdf/platform/protocol/go v0.2.26 h1:22ugJFhAjlz7BRAky3eBljIQrsLzmsdkKVM+pjuG09k= +github.com/opentdf/platform/protocol/go v0.2.26/go.mod h1:eldxqX2oF2ADtG8ivhfwn1lALVMX4aaUM+Lp9ynOJXs= github.com/opentdf/platform/sdk v0.3.25 h1:dZEVeWKfbjrnEXKzSado8ebpzIrk2n6R7RSZRbX+FwE= github.com/opentdf/platform/sdk v0.3.25/go.mod h1:F+RGbT2o9GlzWH9s8VkZyUNUEEAWA3V2RSs8jNQHbqM= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= diff --git a/pkg/cli/confirm.go b/pkg/cli/confirm.go index 23275110..1454e315 100644 --- a/pkg/cli/confirm.go +++ b/pkg/cli/confirm.go @@ -20,6 +20,7 @@ const ( // text input names InputNameFQN = "fully qualified name (FQN)" InputNameFQNUpdated = "deprecated fully qualified name (FQN) being altered" + InputNameKeyID = "key ID" ) func ConfirmAction(action, resource, id string, force bool) { diff --git a/pkg/cli/style.go b/pkg/cli/style.go index 91d8c23c..ed3335c8 100644 --- a/pkg/cli/style.go +++ b/pkg/cli/style.go @@ -8,32 +8,35 @@ type Color struct { Background lipgloss.CompleteAdaptiveColor } -var colorRed = Color{ - Foreground: lipgloss.CompleteAdaptiveColor{ - Light: lipgloss.CompleteColor{ - TrueColor: "#FF0000", - ANSI256: "9", - ANSI: "1", - }, - Dark: lipgloss.CompleteColor{ - TrueColor: "#FF0000", - ANSI256: "9", - ANSI: "1", - }, - }, - Background: lipgloss.CompleteAdaptiveColor{ - Light: lipgloss.CompleteColor{ - TrueColor: "#FFD2D2", - ANSI256: "224", - ANSI: "7", - }, - Dark: lipgloss.CompleteColor{ - TrueColor: "#da6b81", - ANSI256: "52", - ANSI: "4", - }, - }, -} +var ( + ColorRed = colorRed + colorRed = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FF0000", + ANSI256: "9", + ANSI: "1", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#FF0000", + ANSI256: "9", + ANSI: "1", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#FFD2D2", + ANSI256: "224", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#da6b81", + ANSI256: "52", + ANSI: "4", + }, + }, + } +) var colorOrange = Color{ Foreground: lipgloss.CompleteAdaptiveColor{ @@ -90,32 +93,35 @@ var colorYellow = Color{ }, } -var colorGreen = Color{ - Foreground: lipgloss.CompleteAdaptiveColor{ - Light: lipgloss.CompleteColor{ - TrueColor: "#008000", - ANSI256: "28", - ANSI: "2", - }, - Dark: lipgloss.CompleteColor{ - TrueColor: "#008000", - ANSI256: "28", - ANSI: "2", - }, - }, - Background: lipgloss.CompleteAdaptiveColor{ - Light: lipgloss.CompleteColor{ - TrueColor: "#D2FFD2", - ANSI256: "157", - ANSI: "7", - }, - Dark: lipgloss.CompleteColor{ - TrueColor: "#29cf68", - ANSI256: "22", - ANSI: "4", - }, - }, -} +var ( + ColorGreen = colorGreen + colorGreen = Color{ + Foreground: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#008000", + ANSI256: "28", + ANSI: "2", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#008000", + ANSI256: "28", + ANSI: "2", + }, + }, + Background: lipgloss.CompleteAdaptiveColor{ + Light: lipgloss.CompleteColor{ + TrueColor: "#D2FFD2", + ANSI256: "157", + ANSI: "7", + }, + Dark: lipgloss.CompleteColor{ + TrueColor: "#29cf68", + ANSI256: "22", + ANSI: "4", + }, + }, + } +) var colorBlue = Color{ Foreground: lipgloss.CompleteAdaptiveColor{ diff --git a/pkg/cli/table.go b/pkg/cli/table.go index 7564bf9a..864783ef 100644 --- a/pkg/cli/table.go +++ b/pkg/cli/table.go @@ -22,7 +22,8 @@ func NewTable(cols ...table.Column) table.Model { BorderRounded(). WithBaseStyle(styleTable). WithNoPagination(). - WithTargetWidth(TermWidth()) + WithTargetWidth(TermWidth()). + WithMultiline(true) } func NewUUIDColumn() table.Column { diff --git a/pkg/handlers/attribute.go b/pkg/handlers/attribute.go index 3596bef1..3398d221 100644 --- a/pkg/handlers/attribute.go +++ b/pkg/handlers/attribute.go @@ -1,12 +1,16 @@ package handlers import ( + "context" + "errors" "fmt" + "github.com/opentdf/otdfctl/pkg/utils" "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" + "google.golang.org/grpc/status" ) // TODO: Might be useful to map out the attribute rule definitions for help text in the CLI and TUI @@ -31,10 +35,20 @@ func (e *CreateAttributeError) Error() string { return "Error creating attribute" } -func (h Handler) GetAttribute(id string) (*policy.Attribute, error) { - resp, err := h.sdk.Attributes.GetAttribute(h.ctx, &attributes.GetAttributeRequest{ - Id: id, - }) +func (h Handler) GetAttribute(identifier string) (*policy.Attribute, error) { + req := &attributes.GetAttributeRequest{} + + if utils.IsUUID(identifier) { + req.Identifier = &attributes.GetAttributeRequest_AttributeId{ + AttributeId: identifier, + } + } else { + req.Identifier = &attributes.GetAttributeRequest_Fqn{ + Fqn: identifier, + } + } + + resp, err := h.sdk.Attributes.GetAttribute(h.ctx, req) if err != nil { return nil, err } @@ -188,3 +202,54 @@ func GetAttributeRuleFromReadableString(rule string) (policy.AttributeRuleTypeEn } return 0, fmt.Errorf("invalid attribute rule: %s, must be one of [%s, %s, %s]", rule, AttributeRuleAllOf, AttributeRuleAnyOf, AttributeRuleHierarchy) } + +func (h Handler) AddPublicKeyToDefinition(ctx context.Context, definition, publicKeyID string) (*attributes.AttributeKey, error) { + ak := &attributes.AttributeKey{ + KeyId: publicKeyID, + } + + if utils.IsUUID(definition) { + ak.AttributeId = definition + } else { + def, err := h.GetAttribute(definition) + if err != nil { + return nil, err + } + ak.AttributeId = def.GetId() + } + + resp, err := h.sdk.Attributes.AssignKeyToAttribute(ctx, &attributes.AssignKeyToAttributeRequest{ + AttributeKey: ak, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + + return resp.GetAttributeKey(), nil +} + +func (h Handler) RemovePublicKeyFromDefinition(ctx context.Context, definition, publicKeyID string) (*attributes.AttributeKey, error) { + ak := &attributes.AttributeKey{ + KeyId: publicKeyID, + } + + if utils.IsUUID(definition) { + ak.AttributeId = definition + } else { + def, err := h.GetAttribute(definition) + if err != nil { + return nil, err + } + ak.AttributeId = def.GetId() + } + + _, err := h.sdk.Attributes.RemoveKeyFromAttribute(ctx, &attributes.RemoveKeyFromAttributeRequest{ + AttributeKey: ak, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + return ak, nil +} diff --git a/pkg/handlers/attributeValues.go b/pkg/handlers/attributeValues.go index a62ff7d6..ca5e77ca 100644 --- a/pkg/handlers/attributeValues.go +++ b/pkg/handlers/attributeValues.go @@ -1,10 +1,15 @@ package handlers import ( + "context" + "errors" + + "github.com/opentdf/otdfctl/pkg/utils" "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" + "google.golang.org/grpc/status" ) func (h *Handler) ListAttributeValues(attributeId string, state common.ActiveStateEnum, limit, offset int32) ([]*policy.Value, *policy.PageResponse, error) { @@ -36,10 +41,20 @@ func (h *Handler) CreateAttributeValue(attributeId string, value string, metadat return h.GetAttributeValue(resp.GetValue().GetId()) } -func (h *Handler) GetAttributeValue(id string) (*policy.Value, error) { - resp, err := h.sdk.Attributes.GetAttributeValue(h.ctx, &attributes.GetAttributeValueRequest{ - Id: id, - }) +func (h *Handler) GetAttributeValue(identifier string) (*policy.Value, error) { + req := &attributes.GetAttributeValueRequest{} + + if utils.IsUUID(identifier) { + req.Identifier = &attributes.GetAttributeValueRequest_ValueId{ + ValueId: identifier, + } + } else { + req.Identifier = &attributes.GetAttributeValueRequest_Fqn{ + Fqn: identifier, + } + } + + resp, err := h.sdk.Attributes.GetAttributeValue(h.ctx, req) if err != nil { return nil, err } @@ -102,3 +117,54 @@ func (h Handler) UnsafeUpdateAttributeValue(id, value string) error { _, err := h.sdk.Unsafe.UnsafeUpdateAttributeValue(h.ctx, req) return err } + +func (h Handler) AddPublicKeyToValue(ctx context.Context, value, publicKeyID string) (*attributes.ValueKey, error) { + av := &attributes.ValueKey{ + KeyId: publicKeyID, + } + + if utils.IsUUID(value) { + av.ValueId = value + } else { + def, err := h.GetAttributeValue(value) + if err != nil { + return nil, err + } + av.ValueId = def.GetId() + } + + resp, err := h.sdk.Attributes.AssignKeyToValue(ctx, &attributes.AssignKeyToValueRequest{ + ValueKey: av, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + + return resp.GetValueKey(), nil +} + +func (h Handler) RemovePublicKeyFromValue(ctx context.Context, value, publicKeyID string) (*attributes.ValueKey, error) { + vk := &attributes.ValueKey{ + KeyId: publicKeyID, + } + + if utils.IsUUID(value) { + vk.ValueId = value + } else { + def, err := h.GetAttributeValue(value) + if err != nil { + return nil, err + } + vk.ValueId = def.GetId() + } + + _, err := h.sdk.Attributes.RemoveKeyFromValue(ctx, &attributes.RemoveKeyFromValueRequest{ + ValueKey: vk, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + return vk, nil +} diff --git a/pkg/handlers/kas-public-keys.go b/pkg/handlers/kas-public-keys.go new file mode 100644 index 00000000..94e90947 --- /dev/null +++ b/pkg/handlers/kas-public-keys.go @@ -0,0 +1,172 @@ +package handlers + +import ( + "errors" + + "github.com/opentdf/otdfctl/pkg/utils" + "github.com/opentdf/platform/protocol/go/common" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/kasregistry" + "github.com/opentdf/platform/protocol/go/policy/unsafe" + "google.golang.org/grpc/status" +) + +const maxKeyIDLength = 32 + +func (h Handler) CreatePublicKey(kas, pk, kid, alg string, metadata *common.MetadataMutable) (*policy.Key, error) { + // Check if alg is valid + algEnum, err := algToEnum(alg) + if err != nil { + return nil, err + } + + // Key ID can't be more than 32 characters + if len(kid) > maxKeyIDLength { + return nil, errors.New("key id must be less than 32 characters") + } + + // Get KAS UUID if it's not a UUID + + if !utils.IsUUID(kas) { + k, err := h.GetKasRegistryEntry(kas) + if err != nil { + return nil, err + } + kas = k.GetId() + } + + // Create the public key + resp, err := h.sdk.KeyAccessServerRegistry.CreatePublicKey(h.ctx, &kasregistry.CreatePublicKeyRequest{ + KasId: kas, + Key: &policy.KasPublicKey{ + Kid: kid, + Alg: algEnum, + Pem: pk, + }, + Metadata: metadata, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + + return h.GetPublicKey(resp.GetKey().GetId()) +} + +func (h Handler) UpdatePublicKey(id string, metadata *common.MetadataMutable, behavior common.MetadataUpdateEnum) (*policy.Key, error) { + resp, err := h.sdk.KeyAccessServerRegistry.UpdatePublicKey(h.ctx, &kasregistry.UpdatePublicKeyRequest{ + Id: id, + Metadata: metadata, + MetadataUpdateBehavior: behavior, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + + return resp.GetKey(), nil +} + +func (h Handler) GetPublicKey(id string) (*policy.Key, error) { + resp, err := h.sdk.KeyAccessServerRegistry.GetPublicKey(h.ctx, &kasregistry.GetPublicKeyRequest{ + Identifier: &kasregistry.GetPublicKeyRequest_Id{Id: id}, + }) + if err != nil { + return nil, err + } + return resp.GetKey(), nil +} + +func (h Handler) ListPublicKeys(kas string, offset, limit int32) ([]*policy.Key, *policy.PageResponse, error) { + req := &kasregistry.ListPublicKeysRequest{ + Pagination: &policy.PageRequest{ + Offset: offset, + Limit: limit, + }, + } + + switch { + case utils.IsUUID(kas): + req.KasFilter = &kasregistry.ListPublicKeysRequest_KasId{KasId: kas} + case utils.IsURI(kas): + req.KasFilter = &kasregistry.ListPublicKeysRequest_KasUri{KasUri: kas} + case kas != "": + req.KasFilter = &kasregistry.ListPublicKeysRequest_KasName{KasName: kas} + } + + resp, err := h.sdk.KeyAccessServerRegistry.ListPublicKeys(h.ctx, req) + if err != nil { + return nil, nil, err + } + return resp.GetKeys(), resp.GetPagination(), nil +} + +func (h Handler) ListPublicKeyMappings(kas, pkID string, offset, limit int32) ([]*kasregistry.ListPublicKeyMappingResponse_PublicKeyMapping, *policy.PageResponse, error) { + req := &kasregistry.ListPublicKeyMappingRequest{ + PublicKeyId: pkID, + Pagination: &policy.PageRequest{ + Offset: offset, + Limit: limit, + }, + } + + switch { + case utils.IsUUID(kas): + req.KasFilter = &kasregistry.ListPublicKeyMappingRequest_KasId{KasId: kas} + case utils.IsURI(kas): + req.KasFilter = &kasregistry.ListPublicKeyMappingRequest_KasUri{KasUri: kas} + case kas != "": + req.KasFilter = &kasregistry.ListPublicKeyMappingRequest_KasName{KasName: kas} + } + + resp, err := h.sdk.KeyAccessServerRegistry.ListPublicKeyMapping(h.ctx, req) + if err != nil { + return nil, nil, err + } + + return resp.GetPublicKeyMappings(), resp.GetPagination(), nil +} + +func (h Handler) DeactivatePublicKey(id string) error { + _, err := h.sdk.KeyAccessServerRegistry.DeactivatePublicKey(h.ctx, &kasregistry.DeactivatePublicKeyRequest{Id: id}) + if err != nil { + s := status.Convert(err) + return errors.New(s.Message()) + } + return nil +} + +func (h Handler) ActivatePublicKey(id string) error { + _, err := h.sdk.KeyAccessServerRegistry.ActivatePublicKey(h.ctx, &kasregistry.ActivatePublicKeyRequest{Id: id}) + if err != nil { + s := status.Convert(err) + return errors.New(s.Message()) + } + return nil +} + +func (h Handler) UnsafeDeletePublicKey(id string) error { + _, err := h.sdk.Unsafe.UnsafeDeletePublicKey(h.ctx, &unsafe.UnsafeDeletePublicKeyRequest{Id: id}) + if err != nil { + s := status.Convert(err) + return errors.New(s.Message()) + } + return nil +} + +func algToEnum(alg string) (policy.KasPublicKeyAlgEnum, error) { + switch alg { + case "rsa:2048": + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048, nil + case "rsa:4096": + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096, nil + case "ec:secp256r1": + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1, nil + case "ec:secp384r1": + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1, nil + case "ec:secp521r1": + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1, nil + default: + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED, errors.New("unsupported algorithm. supported algorithms are rsa:2048, rsa:4096, ec:secp256r1, ec:secp384r1, ec:secp521r1") + } +} diff --git a/pkg/handlers/kas-registry.go b/pkg/handlers/kas-registry.go index 7c0fbe0d..492edef6 100644 --- a/pkg/handlers/kas-registry.go +++ b/pkg/handlers/kas-registry.go @@ -1,15 +1,26 @@ package handlers import ( + "github.com/opentdf/otdfctl/pkg/utils" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/kasregistry" ) -func (h Handler) GetKasRegistryEntry(id string) (*policy.KeyAccessServer, error) { - resp, err := h.sdk.KeyAccessServerRegistry.GetKeyAccessServer(h.ctx, &kasregistry.GetKeyAccessServerRequest{ - Id: id, - }) +func (h Handler) GetKasRegistryEntry(identifier string) (*policy.KeyAccessServer, error) { + req := &kasregistry.GetKeyAccessServerRequest{} + + switch { + case utils.IsUUID(identifier): + req.Identifier = &kasregistry.GetKeyAccessServerRequest_KasId{KasId: identifier} + case utils.IsURI(identifier): + req.Identifier = &kasregistry.GetKeyAccessServerRequest_Uri{Uri: identifier} + default: + // At this point we assume its a kas name + req.Identifier = &kasregistry.GetKeyAccessServerRequest_Name{Name: identifier} + } + + resp, err := h.sdk.KeyAccessServerRegistry.GetKeyAccessServer(h.ctx, req) if err != nil { return nil, err } diff --git a/pkg/handlers/namespaces.go b/pkg/handlers/namespaces.go index fe773472..eafaf110 100644 --- a/pkg/handlers/namespaces.go +++ b/pkg/handlers/namespaces.go @@ -1,16 +1,31 @@ package handlers import ( + "context" + "errors" + + "github.com/opentdf/otdfctl/pkg/utils" "github.com/opentdf/platform/protocol/go/common" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/namespaces" "github.com/opentdf/platform/protocol/go/policy/unsafe" + "google.golang.org/grpc/status" ) -func (h Handler) GetNamespace(id string) (*policy.Namespace, error) { - resp, err := h.sdk.Namespaces.GetNamespace(h.ctx, &namespaces.GetNamespaceRequest{ - Id: id, - }) +func (h Handler) GetNamespace(identifier string) (*policy.Namespace, error) { + nsReq := new(namespaces.GetNamespaceRequest) + + if utils.IsUUID(identifier) { + nsReq.Identifier = &namespaces.GetNamespaceRequest_NamespaceId{ + NamespaceId: identifier, + } + } else { + nsReq.Identifier = &namespaces.GetNamespaceRequest_Fqn{ + Fqn: identifier, + } + } + + resp, err := h.sdk.Namespaces.GetNamespace(h.ctx, nsReq) if err != nil { return nil, err } @@ -104,3 +119,54 @@ func (h Handler) UnsafeUpdateNamespace(id, name string) (*policy.Namespace, erro return h.GetNamespace(id) } + +func (h Handler) AddPublicKeyToNamespace(ctx context.Context, nameSpace, publicKeyID string) (*namespaces.NamespaceKey, error) { + nk := &namespaces.NamespaceKey{ + KeyId: publicKeyID, + } + + if utils.IsUUID(nameSpace) { + nk.NamespaceId = nameSpace + } else { + nss, err := h.GetNamespace(nameSpace) + if err != nil { + return nil, err + } + nk.NamespaceId = nss.GetId() + } + + resp, err := h.sdk.Namespaces.AssignKeyToNamespace(ctx, &namespaces.AssignKeyToNamespaceRequest{ + NamespaceKey: nk, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + + return resp.GetNamespaceKey(), nil +} + +func (h Handler) RemovePublicKeyFromNamespace(ctx context.Context, nameSpace, publicKeyID string) (*namespaces.NamespaceKey, error) { + nk := &namespaces.NamespaceKey{ + KeyId: publicKeyID, + } + + if utils.IsUUID(nameSpace) { + nk.NamespaceId = nameSpace + } else { + nss, err := h.GetNamespace(nameSpace) + if err != nil { + return nil, err + } + nk.NamespaceId = nss.GetId() + } + _, err := h.sdk.Namespaces.RemoveKeyFromNamespace(ctx, &namespaces.RemoveKeyFromNamespaceRequest{ + NamespaceKey: nk, + }) + if err != nil { + s := status.Convert(err) + return nil, errors.New(s.Message()) + } + + return nk, nil +} diff --git a/pkg/utils/validators.go b/pkg/utils/validators.go index 8569aaf9..245fab79 100644 --- a/pkg/utils/validators.go +++ b/pkg/utils/validators.go @@ -4,6 +4,8 @@ import ( "errors" "net/url" "strings" + + "github.com/google/uuid" ) func NormalizeEndpoint(endpoint string) (*url.URL, error) { @@ -31,3 +33,16 @@ func NormalizeEndpoint(endpoint string) (*url.URL, error) { } return u, nil } + +func IsUUID(u string) bool { + _, err := uuid.Parse(u) + return err == nil +} + +func IsURI(u string) bool { + ur, err := url.Parse(u) + if err != nil { + return false + } + return ur.IsAbs() +}