Skip to content

Commit 26f6fcc

Browse files
authored
feat(core): subject condition set CLI CRUD (#78)
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.
1 parent f53b61d commit 26f6fcc

5 files changed

+384
-2
lines changed

cmd/policy-attributes.go

-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ func init() {
216216
policy_attributesCreateCmd.Flags().StringP("rule", "r", "", "Rule of the attribute")
217217
policy_attributesCreateCmd.Flags().StringSliceVarP(&attrValues, "values", "v", []string{}, "Values of the attribute")
218218
policy_attributesCreateCmd.Flags().StringP("namespace", "s", "", "Namespace of the attribute")
219-
policy_attributesCreateCmd.Flags().StringP("description", "d", "", "Description of the attribute")
220219
injectLabelFlags(policy_attributesCreateCmd, false)
221220

222221
// Get an attribute

cmd/policy-subject_condition_sets.go

+299
Original file line numberDiff line numberDiff line change
@@ -1 +1,300 @@
11
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
"strings"
9+
10+
"github.com/opentdf/platform/protocol/go/policy"
11+
"github.com/opentdf/tructl/pkg/cli"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
var (
16+
policy_subject_condition_setsCmds = []string{
17+
policy_subject_condition_setCreateCmd.Use,
18+
policy_subject_condition_setGetCmd.Use,
19+
policy_subject_condition_setListCmd.Use,
20+
policy_subject_condition_setUpdateCmd.Use,
21+
policy_subject_condition_setDeleteCmd.Use,
22+
}
23+
24+
policy_subject_condition_setCmd = &cobra.Command{
25+
Use: "subject-condition-sets",
26+
Short: "Manage subject condition sets" + strings.Join(policy_subject_condition_setsCmds, ", ") + "]",
27+
Long: `
28+
Subject Condition Sets - fields and values known to an external user source that are utilized to relate a Subject (PE/NPE) to
29+
a Subject Mapping and, by said mapping, an Attribute Value.`,
30+
}
31+
32+
policy_subject_condition_setCreateCmd = &cobra.Command{
33+
Use: "create",
34+
Short: "Create a subject condition set",
35+
Run: func(cmd *cobra.Command, args []string) {
36+
h := cli.NewHandler(cmd)
37+
defer h.Close()
38+
var (
39+
ss []*policy.SubjectSet
40+
ssBytes []byte
41+
)
42+
43+
flagHelper := cli.NewFlagHelper(cmd)
44+
ssFlagJSON := flagHelper.GetOptionalString("subject-sets")
45+
ssFileJSON := flagHelper.GetOptionalString("subject-sets-file-json")
46+
metadataLabels := flagHelper.GetStringSlice("label", metadataLabels, cli.FlagHelperStringSliceOptions{Min: 0})
47+
48+
// validate no flag conflicts
49+
if ssFileJSON == "" && ssFlagJSON == "" {
50+
cli.ExitWithError("At least one subject set must be provided ('--subject-sets', '--subject-sets-file-json')", nil)
51+
} else if ssFileJSON != "" && ssFlagJSON != "" {
52+
cli.ExitWithError("Only one of '--subject-sets' or '--subject-sets-file-json' can be provided", nil)
53+
}
54+
55+
// read subject sets into bytes from either the flagged json file or json string
56+
if ssFileJSON != "" {
57+
jsonFile, err := os.Open(ssFileJSON)
58+
if err != nil {
59+
cli.ExitWithError(fmt.Sprintf("Failed to open file at path: %s", ssFileJSON), err)
60+
}
61+
defer jsonFile.Close()
62+
63+
bytes, err := ioutil.ReadAll(jsonFile)
64+
if err != nil {
65+
cli.ExitWithError(fmt.Sprintf("Failed to read bytes from file at path: %s", ssFileJSON), err)
66+
}
67+
ssBytes = bytes
68+
} else {
69+
ssBytes = []byte(ssFlagJSON)
70+
}
71+
72+
if err := json.Unmarshal(ssBytes, &ss); err != nil {
73+
cli.ExitWithError("Error unmarshalling subject sets", err)
74+
}
75+
76+
scs, err := h.CreateSubjectConditionSet(ss, getMetadataMutable(metadataLabels))
77+
if err != nil {
78+
cli.ExitWithError("Error creating subject condition set", err)
79+
}
80+
81+
var subjectSetsJSON []byte
82+
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
83+
cli.ExitWithError("Error marshalling subject condition set", err)
84+
}
85+
86+
rows := [][]string{
87+
{"Id", scs.Id},
88+
{"SubjectSets", string(subjectSetsJSON)},
89+
}
90+
91+
if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
92+
rows = append(rows, mdRows...)
93+
}
94+
95+
t := cli.NewTabular().Rows(rows...)
96+
HandleSuccess(cmd, scs.Id, t, scs)
97+
},
98+
}
99+
100+
policy_subject_condition_setGetCmd = &cobra.Command{
101+
Use: "get",
102+
Short: "Get a subject condition set by id",
103+
Run: func(cmd *cobra.Command, args []string) {
104+
h := cli.NewHandler(cmd)
105+
defer h.Close()
106+
107+
flagHelper := cli.NewFlagHelper(cmd)
108+
id := flagHelper.GetRequiredString("id")
109+
110+
scs, err := h.GetSubjectConditionSet(id)
111+
if err != nil {
112+
cli.ExitWithNotFoundError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err)
113+
}
114+
115+
var subjectSetsJSON []byte
116+
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
117+
cli.ExitWithError("Error marshalling subject condition set", err)
118+
}
119+
120+
rows := [][]string{
121+
{"Id", scs.Id},
122+
{"SubjectSets", string(subjectSetsJSON)},
123+
}
124+
125+
if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
126+
rows = append(rows, mdRows...)
127+
}
128+
129+
t := cli.NewTabular().Rows(rows...)
130+
HandleSuccess(cmd, scs.Id, t, scs)
131+
},
132+
}
133+
134+
policy_subject_condition_setListCmd = &cobra.Command{
135+
Use: "list",
136+
Short: "List subject condition sets",
137+
Run: func(cmd *cobra.Command, args []string) {
138+
h := cli.NewHandler(cmd)
139+
defer h.Close()
140+
141+
scsList, err := h.ListSubjectConditionSets()
142+
if err != nil {
143+
cli.ExitWithError("Error listing subject condition sets", err)
144+
}
145+
146+
t := cli.NewTable()
147+
t.Headers("Id", "SubjectSets")
148+
for _, scs := range scsList {
149+
var subjectSetsJSON []byte
150+
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
151+
cli.ExitWithError("Error marshalling subject condition set", err)
152+
}
153+
rowCells := []string{scs.Id, string(subjectSetsJSON)}
154+
t.Row(rowCells...)
155+
}
156+
157+
HandleSuccess(cmd, "", t, scsList)
158+
},
159+
}
160+
161+
policy_subject_condition_setUpdateCmd = &cobra.Command{
162+
Use: "update",
163+
Short: "Update a subject condition set",
164+
Run: func(cmd *cobra.Command, args []string) {
165+
h := cli.NewHandler(cmd)
166+
defer h.Close()
167+
168+
flagHelper := cli.NewFlagHelper(cmd)
169+
id := flagHelper.GetRequiredString("id")
170+
metadataLabels := flagHelper.GetStringSlice("label", metadataLabels, cli.FlagHelperStringSliceOptions{Min: 0})
171+
ssFlagJSON := flagHelper.GetOptionalString("subject-sets")
172+
173+
var ss []*policy.SubjectSet
174+
if err := json.Unmarshal([]byte(ssFlagJSON), &ss); err != nil {
175+
cli.ExitWithError("Error unmarshalling subject sets", err)
176+
}
177+
178+
_, err := h.UpdateSubjectConditionSet(id, ss, getMetadataMutable(metadataLabels), getMetadataUpdateBehavior())
179+
if err != nil {
180+
cli.ExitWithError("Error updating subject condition set", err)
181+
}
182+
183+
scs, err := h.GetSubjectConditionSet(id)
184+
if err != nil {
185+
cli.ExitWithError("Error getting subject condition set", err)
186+
}
187+
188+
var subjectSetsJSON []byte
189+
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
190+
cli.ExitWithError("Error marshalling subject condition set", err)
191+
}
192+
193+
rows := [][]string{
194+
{"Id", scs.Id},
195+
{"SubjectSets", string(subjectSetsJSON)},
196+
}
197+
198+
if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
199+
rows = append(rows, mdRows...)
200+
}
201+
202+
t := cli.NewTabular().Rows(rows...)
203+
HandleSuccess(cmd, scs.Id, t, scs)
204+
},
205+
}
206+
207+
policy_subject_condition_setDeleteCmd = &cobra.Command{
208+
Use: "delete",
209+
Short: "Delete a subject condition set",
210+
Run: func(cmd *cobra.Command, args []string) {
211+
h := cli.NewHandler(cmd)
212+
defer h.Close()
213+
214+
flagHelper := cli.NewFlagHelper(cmd)
215+
id := flagHelper.GetRequiredString("id")
216+
217+
scs, err := h.GetSubjectConditionSet(id)
218+
if err != nil {
219+
cli.ExitWithNotFoundError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err)
220+
}
221+
222+
cli.ConfirmDelete("Subject Condition Set", id)
223+
224+
if err := h.DeleteSubjectConditionSet(id); err != nil {
225+
cli.ExitWithNotFoundError(fmt.Sprintf("Subject Condition Set with id %s not found", id), err)
226+
}
227+
228+
var subjectSetsJSON []byte
229+
if subjectSetsJSON, err = json.Marshal(scs.SubjectSets); err != nil {
230+
cli.ExitWithError("Error marshalling subject condition set", err)
231+
}
232+
233+
rows := [][]string{
234+
{"Id", scs.Id},
235+
{"SubjectSets", string(subjectSetsJSON)},
236+
}
237+
238+
if mdRows := getMetadataRows(scs.Metadata); mdRows != nil {
239+
rows = append(rows, mdRows...)
240+
}
241+
242+
t := cli.NewTabular().Rows(rows...)
243+
HandleSuccess(cmd, scs.Id, t, scs)
244+
},
245+
}
246+
)
247+
248+
func init() {
249+
policyCmd.AddCommand(policy_subject_condition_setCmd)
250+
251+
policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setCreateCmd)
252+
injectLabelFlags(policy_subject_condition_setCreateCmd, false)
253+
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.")
254+
policy_subject_condition_setCreateCmd.Flags().StringP("subject-sets-file-json", "j", "", "A JSON file with path from $HOME containing an array of subject sets")
255+
256+
policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setGetCmd)
257+
policy_subject_condition_setGetCmd.Flags().StringP("id", "i", "", "Id of the subject condition set")
258+
259+
policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setListCmd)
260+
261+
policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setUpdateCmd)
262+
policy_subject_condition_setUpdateCmd.Flags().StringP("id", "i", "", "Id of the subject condition set")
263+
injectLabelFlags(policy_subject_condition_setUpdateCmd, true)
264+
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.")
265+
266+
policy_subject_condition_setCmd.AddCommand(policy_subject_condition_setDeleteCmd)
267+
policy_subject_condition_setDeleteCmd.Flags().StringP("id", "i", "", "Id of the subject condition set")
268+
}
269+
270+
func getSubjectConditionSetOperatorFromChoice(choice string) (policy.SubjectMappingOperatorEnum, error) {
271+
switch choice {
272+
case "IN":
273+
return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, nil
274+
case "NOT_IN":
275+
return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_NOT_IN, nil
276+
default:
277+
return policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_UNSPECIFIED, fmt.Errorf("Unknown operator must be specified ['IN', 'NOT_IN']: %s", choice)
278+
}
279+
}
280+
281+
func getSubjectConditionSetBooleanTypeFromChoice(choice string) (policy.ConditionBooleanTypeEnum, error) {
282+
switch choice {
283+
case "AND":
284+
return policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, nil
285+
case "OR":
286+
return policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_OR, nil
287+
default:
288+
return policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_UNSPECIFIED, fmt.Errorf("Unknown boolean type must be specified ['AND', 'OR']: %s", choice)
289+
}
290+
}
291+
292+
func getMarshaledSubjectSets(subjectSets string) ([]*policy.SubjectSet, error) {
293+
var ss []*policy.SubjectSet
294+
295+
if err := json.Unmarshal([]byte(subjectSets), &ss); err != nil {
296+
return nil, err
297+
}
298+
299+
return ss, nil
300+
}

cmd/policy-subject_mappings.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ Note: SubjectConditionSets are reusable among SubjectMappings and are available
154154
for _, a := range standardActions {
155155
a = strings.ToUpper(a)
156156
if a != "DECRYPT" && a != "TRANSMIT" {
157-
cli.ExitWithError(fmt.Sprintf("Invalid Standard Action: '%s'. Must be one of [ENCRYPT, TRANSMIT].", a), nil)
157+
cli.ExitWithError(fmt.Sprintf("Invalid Standard Action: '%s'. Must be one of [DECRYPT, TRANSMIT].", a), nil)
158158
}
159159
}
160160
}

pkg/cli/flagValues.go

+25
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,28 @@ func (f FlagHelper) GetRequiredInt32(flag string) int32 {
5757
// }
5858
return v
5959
}
60+
61+
// func (f FlagHelper) GetStructSlice(flag string, v []StructFlag[T], opts FlagHelperStringSliceOptions) ([]StructFlag[T], err) {
62+
// if len(v) < opts.Min {
63+
// fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at least %d non-empty values", flag, opts.Min), nil))
64+
// os.Exit(1)
65+
// }
66+
// if opts.Max > 0 && len(v) > opts.Max {
67+
// fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at most %d non-empty values", flag, opts.Max), nil))
68+
// os.Exit(1)
69+
// }
70+
// return v
71+
// }
72+
73+
// type StructFlag[T any] struct {
74+
// Val T
75+
// }
76+
77+
// func (this StructFlag[T]) String() string {
78+
// b, _ := json.Marshal(this)
79+
// return string(b)
80+
// }
81+
82+
// func (this StructFlag[T]) Set(s string) error {
83+
// return json.Unmarshal([]byte(s), this)
84+
// }

0 commit comments

Comments
 (0)