Skip to content

Commit

Permalink
feat(core): subject condition set CLI CRUD (#78)
Browse files Browse the repository at this point in the history
Adds CRUD for subject condition sets, with the JSON relation of
`[]*policy.SubjectSets` passed via a string flag or found in a `.json`
file with the filepath/name provided in a flag on CREATE, and validation
that only one is provided at once. On UPDATE, only a JSON string is
allowed.

There is an open `pflags` issue (since 2022) which is the library under
Cobra's flags implementation which affects the Subject Condition Sets
(SCSs) flag API: spf13/pflag#370.

Unfortunately, this issue means we cannot allow a slice of individual
SCSs passed via CLI as we do with `--label` where each individual label
passed with `--label` populates a `[]string` of all `labels`. In this
case, if we attempt `--subject-set <single subject set json>` to
populate a `[]string` where each index is a JSON string for a single
SCS, we get an error `flag: parse error on line 1, column 3: bare " in
non-quoted-field`. Because of this, we must expect all SCSs being
created via JSON in the CLI to already be joined into the single array
and passed as a single string flag `--subject-sets <json array of all
subject sets in the SCS>`.

There is already support added in this PR for reading from a JSON file
to create the SCS, and any time there is JSON in the CLI it is likely it
will be added via script instead of manually.

See [new issue](#77) around
admin UX of testing Subject Condition Sets before creating.

> [!NOTE]
> This PR was going to introduce reading Subject Sets from a YAML file
as well, but yaml struct tags are not generated in the proto-built
types. If this is needed, it should be discussed further and separately
how the platform could expose YAML tags so consumers do not reimplement
them repeatedly and potentially mistakenly. Perhaps a [new proto
plugin](https://github.com/srikrsna/protoc-gen-gotag) could be utilized.
  • Loading branch information
jakedoublev authored Mar 28, 2024
1 parent f53b61d commit 26f6fcc
Show file tree
Hide file tree
Showing 5 changed files with 384 additions and 2 deletions.
1 change: 0 additions & 1 deletion cmd/policy-attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ func init() {
policy_attributesCreateCmd.Flags().StringP("rule", "r", "", "Rule of the attribute")
policy_attributesCreateCmd.Flags().StringSliceVarP(&attrValues, "values", "v", []string{}, "Values of the attribute")
policy_attributesCreateCmd.Flags().StringP("namespace", "s", "", "Namespace of the attribute")
policy_attributesCreateCmd.Flags().StringP("description", "d", "", "Description of the attribute")
injectLabelFlags(policy_attributesCreateCmd, false)

// Get an attribute
Expand Down
299 changes: 299 additions & 0 deletions cmd/policy-subject_condition_sets.go
Original file line number Diff line number Diff line change
@@ -1 +1,300 @@
package cmd

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"

"github.com/opentdf/platform/protocol/go/policy"
"github.com/opentdf/tructl/pkg/cli"
"github.com/spf13/cobra"
)

var (
policy_subject_condition_setsCmds = []string{
policy_subject_condition_setCreateCmd.Use,
policy_subject_condition_setGetCmd.Use,
policy_subject_condition_setListCmd.Use,
policy_subject_condition_setUpdateCmd.Use,
policy_subject_condition_setDeleteCmd.Use,
}

policy_subject_condition_setCmd = &cobra.Command{
Use: "subject-condition-sets",
Short: "Manage subject condition sets" + strings.Join(policy_subject_condition_setsCmds, ", ") + "]",
Long: `
Subject Condition Sets - fields and values known to an external user source that are utilized to relate a Subject (PE/NPE) to
a Subject Mapping and, by said mapping, an Attribute Value.`,
}

policy_subject_condition_setCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a subject condition set",
Run: func(cmd *cobra.Command, args []string) {
h := cli.NewHandler(cmd)
defer h.Close()
var (
ss []*policy.SubjectSet
ssBytes []byte
)

flagHelper := cli.NewFlagHelper(cmd)
ssFlagJSON := flagHelper.GetOptionalString("subject-sets")
ssFileJSON := flagHelper.GetOptionalString("subject-sets-file-json")
metadataLabels := flagHelper.GetStringSlice("label", metadataLabels, cli.FlagHelperStringSliceOptions{Min: 0})

// validate no flag conflicts
if ssFileJSON == "" && ssFlagJSON == "" {
cli.ExitWithError("At least one subject set must be provided ('--subject-sets', '--subject-sets-file-json')", nil)
} else if ssFileJSON != "" && ssFlagJSON != "" {
cli.ExitWithError("Only one of '--subject-sets' or '--subject-sets-file-json' can be provided", nil)
}

// read subject sets into bytes from either the flagged json file or json string
if ssFileJSON != "" {
jsonFile, err := os.Open(ssFileJSON)
if err != nil {
cli.ExitWithError(fmt.Sprintf("Failed to open file at path: %s", ssFileJSON), err)
}
defer jsonFile.Close()

bytes, err := ioutil.ReadAll(jsonFile)
if err != nil {
cli.ExitWithError(fmt.Sprintf("Failed to read bytes from file at path: %s", ssFileJSON), err)
}
ssBytes = bytes
} else {
ssBytes = []byte(ssFlagJSON)
}

if err := json.Unmarshal(ssBytes, &ss); err != nil {
cli.ExitWithError("Error unmarshalling subject sets", err)
}

scs, err := h.CreateSubjectConditionSet(ss, getMetadataMutable(metadataLabels))
if err != nil {
cli.ExitWithError("Error creating subject condition set", err)
}

var subjectSetsJSON []byte
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
cli.ExitWithError("Error marshalling subject condition set", err)
}

rows := [][]string{
{"Id", scs.Id},
{"SubjectSets", string(subjectSetsJSON)},
}

if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
rows = append(rows, mdRows...)
}

t := cli.NewTabular().Rows(rows...)
HandleSuccess(cmd, scs.Id, t, scs)
},
}

policy_subject_condition_setGetCmd = &cobra.Command{
Use: "get",
Short: "Get a subject condition set by id",
Run: func(cmd *cobra.Command, args []string) {
h := cli.NewHandler(cmd)
defer h.Close()

flagHelper := cli.NewFlagHelper(cmd)
id := flagHelper.GetRequiredString("id")

scs, err := h.GetSubjectConditionSet(id)
if err != nil {
cli.ExitWithNotFoundError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err)
}

var subjectSetsJSON []byte
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
cli.ExitWithError("Error marshalling subject condition set", err)
}

rows := [][]string{
{"Id", scs.Id},
{"SubjectSets", string(subjectSetsJSON)},
}

if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
rows = append(rows, mdRows...)
}

t := cli.NewTabular().Rows(rows...)
HandleSuccess(cmd, scs.Id, t, scs)
},
}

policy_subject_condition_setListCmd = &cobra.Command{
Use: "list",
Short: "List subject condition sets",
Run: func(cmd *cobra.Command, args []string) {
h := cli.NewHandler(cmd)
defer h.Close()

scsList, err := h.ListSubjectConditionSets()
if err != nil {
cli.ExitWithError("Error listing subject condition sets", err)
}

t := cli.NewTable()
t.Headers("Id", "SubjectSets")
for _, scs := range scsList {
var subjectSetsJSON []byte
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
cli.ExitWithError("Error marshalling subject condition set", err)
}
rowCells := []string{scs.Id, string(subjectSetsJSON)}
t.Row(rowCells...)
}

HandleSuccess(cmd, "", t, scsList)
},
}

policy_subject_condition_setUpdateCmd = &cobra.Command{
Use: "update",
Short: "Update a subject condition set",
Run: func(cmd *cobra.Command, args []string) {
h := cli.NewHandler(cmd)
defer h.Close()

flagHelper := cli.NewFlagHelper(cmd)
id := flagHelper.GetRequiredString("id")
metadataLabels := flagHelper.GetStringSlice("label", metadataLabels, cli.FlagHelperStringSliceOptions{Min: 0})
ssFlagJSON := flagHelper.GetOptionalString("subject-sets")

var ss []*policy.SubjectSet
if err := json.Unmarshal([]byte(ssFlagJSON), &ss); err != nil {
cli.ExitWithError("Error unmarshalling subject sets", err)
}

_, err := h.UpdateSubjectConditionSet(id, ss, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior())
if err != nil {
cli.ExitWithError("Error updating subject condition set", err)
}

scs, err := h.GetSubjectConditionSet(id)
if err != nil {
cli.ExitWithError("Error getting subject condition set", err)
}

var subjectSetsJSON []byte
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
cli.ExitWithError("Error marshalling subject condition set", err)
}

rows := [][]string{
{"Id", scs.Id},
{"SubjectSets", string(subjectSetsJSON)},
}

if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
rows = append(rows, mdRows...)
}

t := cli.NewTabular().Rows(rows...)
HandleSuccess(cmd, scs.Id, t, scs)
},
}

policy_subject_condition_setDeleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a subject condition set",
Run: func(cmd *cobra.Command, args []string) {
h := cli.NewHandler(cmd)
defer h.Close()

flagHelper := cli.NewFlagHelper(cmd)
id := flagHelper.GetRequiredString("id")

scs, err := h.GetSubjectConditionSet(id)
if err != nil {
cli.ExitWithNotFoundError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err)
}

cli.ConfirmDelete("Subject Condition Set", id)

if err := h.DeleteSubjectConditionSet(id); err != nil {
cli.ExitWithNotFoundError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err)
}

var subjectSetsJSON []byte
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
cli.ExitWithError("Error marshalling subject condition set", err)
}

rows := [][]string{
{"Id", scs.Id},
{"SubjectSets", string(subjectSetsJSON)},
}

if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
rows = append(rows, mdRows...)
}

t := cli.NewTabular().Rows(rows...)
HandleSuccess(cmd, scs.Id, t, scs)
},
}
)

func init() {
policyCmd.AddCommand(policy_subject_condition_setCmd)

policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setCreateCmd)
injectLabelFlags(policy_subject_condition_setCreateCmd, false)
policy_subject_condition_setCreateCmd.Flags().StringP("subject-sets", "s", "", "A JSON array of subject sets, containing a list of condition groups, each with one or more conditions.")
policy_subject_condition_setCreateCmd.Flags().StringP("subject-sets-file-json", "j", "", "A JSON file with path from $HOME containing an array of subject sets")

policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setGetCmd)
policy_subject_condition_setGetCmd.Flags().StringP("id", "i", "", "Id of the subject condition set")

policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setListCmd)

policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setUpdateCmd)
policy_subject_condition_setUpdateCmd.Flags().StringP("id", "i", "", "Id of the subject condition set")
injectLabelFlags(policy_subject_condition_setUpdateCmd, true)
policy_subject_condition_setUpdateCmd.Flags().StringP("subject-sets", "s", "", "A JSON array of subject sets, containing a list of condition groups, each with one or more conditions.")

policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setDeleteCmd)
policy_subject_condition_setDeleteCmd.Flags().StringP("id", "i", "", "Id of the subject condition set")
}

func getSubjectConditionSetOperatorFromChoice(choice string) (policy.SubjectMappingOperatorEnum, error) {
switch choice {
case "IN":
return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, nil
case "NOT_IN":
return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN, nil
default:
return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED, fmt.Errorf("Unknown operator must be specified ['IN', 'NOT_IN']: %s", choice)
}
}

func getSubjectConditionSetBooleanTypeFromChoice(choice string) (policy.ConditionBooleanTypeEnum, error) {
switch choice {
case "AND":
return policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, nil
case "OR":
return policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR, nil
default:
return policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED, fmt.Errorf("Unknown boolean type must be specified ['AND', 'OR']: %s", choice)
}
}

func getMarshaledSubjectSets(subjectSets string) ([]*policy.SubjectSet, error) {
var ss []*policy.SubjectSet

if err := json.Unmarshal([]byte(subjectSets), &ss); err != nil {
return nil, err
}

return ss, nil
}
2 changes: 1 addition & 1 deletion cmd/policy-subject_mappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Note: SubjectConditionSets are reusable among SubjectMappings and are available
for _, a := range standardActions {
a = strings.ToUpper(a)
if a != "DECRYPT" && a != "TRANSMIT" {
cli.ExitWithError(fmt.Sprintf("Invalid Standard Action: '%s'. Must be one of [ENCRYPT, TRANSMIT].", a), nil)
cli.ExitWithError(fmt.Sprintf("Invalid Standard Action: '%s'. Must be one of [DECRYPT, TRANSMIT].", a), nil)
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/cli/flagValues.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,28 @@ func (f FlagHelper) GetRequiredInt32(flag string) int32 {
// }
return v
}

// func (f FlagHelper) GetStructSlice(flag string, v []StructFlag[T], opts FlagHelperStringSliceOptions) ([]StructFlag[T], err) {
// if len(v) < opts.Min {
// fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at least %d non-empty values", flag, opts.Min), nil))
// os.Exit(1)
// }
// if opts.Max > 0 && len(v) > opts.Max {
// fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at most %d non-empty values", flag, opts.Max), nil))
// os.Exit(1)
// }
// return v
// }

// type StructFlag[T any] struct {
// Val T
// }

// func (this StructFlag[T]) String() string {
// b, _ := json.Marshal(this)
// return string(b)
// }

// func (this StructFlag[T]) Set(s string) error {
// return json.Unmarshal([]byte(s), this)
// }
Loading

0 comments on commit 26f6fcc

Please sign in to comment.