Skip to content

Commit bc56c19

Browse files
authored
feat(core): add subject-mappings match to CLI (#413)
Unblocked by the merge of opentdf/platform#1658 Closes #410
1 parent 79f2079 commit bc56c19

File tree

9 files changed

+178
-10
lines changed

9 files changed

+178
-10
lines changed

.github/spellcheck.ignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ SCS
3636
SCSs
3737
SDK
3838
ShinyThing
39+
SubjectConditionSets
3940
TDF
4041
TDF'd
4142
TDFd

cmd/policy-subjectMappings.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/evertras/bubble-table/table"
99
"github.com/opentdf/otdfctl/pkg/cli"
10+
"github.com/opentdf/otdfctl/pkg/handlers"
1011
"github.com/opentdf/otdfctl/pkg/man"
1112
"github.com/opentdf/platform/protocol/go/policy"
1213
"github.com/opentdf/platform/protocol/go/policy/subjectmapping"
@@ -253,6 +254,70 @@ func policy_updateSubjectMapping(cmd *cobra.Command, args []string) {
253254
HandleSuccess(cmd, id, t, updated)
254255
}
255256

257+
func policy_matchSubjectMappings(cmd *cobra.Command, args []string) {
258+
c := cli.New(cmd, args)
259+
h := NewHandler(c)
260+
defer h.Close()
261+
262+
subject := c.Flags.GetOptionalString("subject")
263+
selectors = c.Flags.GetStringSlice("selector", selectors, cli.FlagsStringSliceOptions{Min: 0})
264+
265+
if len(selectors) > 0 && subject != "" {
266+
cli.ExitWithError("Must provide either '--subject' or '--selector' flag values, not both", nil)
267+
}
268+
269+
if subject != "" {
270+
flattened, err := handlers.FlattenSubjectContext(subject)
271+
if err != nil {
272+
cli.ExitWithError("Could not process '--subject' value", err)
273+
}
274+
for _, item := range flattened {
275+
selectors = append(selectors, item.Key)
276+
}
277+
}
278+
279+
matched, err := h.MatchSubjectMappings(selectors)
280+
if err != nil {
281+
cli.ExitWithError(fmt.Sprintf("Failed to match subject mappings with selectors %v", selectors), err)
282+
}
283+
284+
t := cli.NewTable(
285+
cli.NewUUIDColumn(),
286+
table.NewFlexColumn("subject_attrval_id", "Subject AttrVal: Id", cli.FlexColumnWidthFour),
287+
table.NewFlexColumn("subject_attrval_value", "Subject AttrVal: Value", cli.FlexColumnWidthThree),
288+
table.NewFlexColumn("actions", "Actions", cli.FlexColumnWidthTwo),
289+
table.NewFlexColumn("subject_condition_set_id", "Subject Condition Set: Id", cli.FlexColumnWidthFour),
290+
table.NewFlexColumn("subject_condition_set", "Subject Condition Set", cli.FlexColumnWidthThree),
291+
)
292+
rows := []table.Row{}
293+
for _, sm := range matched {
294+
var actionsJSON []byte
295+
if actionsJSON, err = json.Marshal(sm.GetActions()); err != nil {
296+
cli.ExitWithError("Error marshalling subject mapping actions", err)
297+
}
298+
299+
var subjectSetsJSON []byte
300+
if subjectSetsJSON, err = json.Marshal(sm.GetSubjectConditionSet().GetSubjectSets()); err != nil {
301+
cli.ExitWithError("Error marshalling subject condition set", err)
302+
}
303+
metadata := cli.ConstructMetadata(sm.GetMetadata())
304+
305+
rows = append(rows, table.NewRow(table.RowData{
306+
"id": sm.GetId(),
307+
"subject_attrval_id": sm.GetAttributeValue().GetId(),
308+
"subject_attrval_value": sm.GetAttributeValue().GetValue(),
309+
"actions": string(actionsJSON),
310+
"subject_condition_set_id": sm.GetSubjectConditionSet().GetId(),
311+
"subject_condition_set": string(subjectSetsJSON),
312+
"labels": metadata["Labels"],
313+
"created_at": metadata["Created At"],
314+
"updated_at": metadata["Updated At"],
315+
}))
316+
}
317+
t = t.WithRows(rows)
318+
HandleSuccess(cmd, "", t, matched)
319+
}
320+
256321
func getSubjectMappingMappingActionEnumFromChoice(readable string) policy.Action_StandardAction {
257322
switch readable {
258323
case actionStandardDecrypt:
@@ -378,13 +443,31 @@ func init() {
378443
deleteDoc.GetDocFlag("force").Description,
379444
)
380445

446+
matchDoc := man.Docs.GetCommand("policy/subject-mappings/match",
447+
man.WithRun(policy_matchSubjectMappings),
448+
)
449+
matchDoc.Flags().StringP(
450+
matchDoc.GetDocFlag("subject").Name,
451+
matchDoc.GetDocFlag("subject").Shorthand,
452+
matchDoc.GetDocFlag("subject").Default,
453+
matchDoc.GetDocFlag("subject").Description,
454+
)
455+
matchDoc.Flags().StringSliceVarP(
456+
&selectors,
457+
matchDoc.GetDocFlag("selector").Name,
458+
matchDoc.GetDocFlag("selector").Shorthand,
459+
[]string{},
460+
matchDoc.GetDocFlag("selector").Description,
461+
)
462+
381463
doc := man.Docs.GetCommand("policy/subject-mappings",
382464
man.WithSubcommands(
383465
createDoc,
384466
getDoc,
385467
listDoc,
386468
updateDoc,
387469
deleteDoc,
470+
matchDoc,
388471
),
389472
)
390473
policy_subjectMappingCmd := &doc.Command

docs/man/auth/client-credentials.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ in the OS keyring for future use.
2626
Authenticate with client credentials (secret provided interactively)
2727

2828
```shell
29-
opentdf auth client-credentials --client-id <client-id>
29+
otdfctl auth client-credentials --client-id <client-id>
3030
```
3131

3232
Authenticate with client credentials (secret provided as argument)
3333

3434
```shell
35-
opentdf auth client-credentials --client-id <client-id> --client-secret <client-secret>
35+
otdfctl auth client-credentials --client-id <client-id> --client-secret <client-secret>
3636
```

docs/man/interactive.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: Interactive Mode
2+
title: Interactive Mode (experimental)
33

44
command:
55
name: interactive
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
title: Match a subject or set of selectors to relevant subject mappings
3+
command:
4+
name: match
5+
flags:
6+
- name: subject
7+
shorthand: s
8+
description: A Subject Entity Representation string (JSON or JWT, auto-detected)
9+
default: ''
10+
- name: selector
11+
shorthand: x
12+
description: "Individual selectors (i.e. '.department' or '.realm_access.roles[]') that may be found in SubjectConditionSets"
13+
---
14+
15+
This tool queries platform policies for relevant Subject Mappings using either an Entity Representation or specific selectors.
16+
17+
If an Entity Representation is provided via `--subject` (such as an OIDC JWT or JSON response from an Entity Resolution Service), the tool
18+
parses all valid selectors and checks for matching Subject Condition Sets in Subject Mappings to Attribute Values.
19+
20+
If selectors are provided directly with `--selector`, the tool searches for Subject Mappings with Subject Condition Sets that contain those selectors.
21+
22+
## Examples
23+
24+
Various ways to invoke the `match` command to query Subject Mappings to Attribute Values with relevant Subject Condition Sets.
25+
26+
```shell
27+
# matches either org name or department selectors
28+
otdfctl policy subject-mappings match --selector '.org.name' --selector '.department'
29+
30+
# parses subject entity representation as JSON and matches any selector (with this subject only '.emailAddress')
31+
otdfctl policy subject-mappings match --subject '{"emailAddress":"[email protected]"}'
32+
33+
# parses entity representation as JWT into all possicle claim selectors and matches any of them
34+
otdfctl policy subject-mappings match --subject 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
35+
```
36+
37+
> [!NOTE]
38+
> The values of the selectors and any `IN`/`NOT_IN`/`IN_CONTAINS` logic of Subject Condition Sets is irrelevant to this command.
39+
> Evaluation of any matched conditions is handled by the Authorization Service to determine entitlements. This command
40+
> is specifically for management of policy - to facilitate lookup of current conditions driven by known selectors as a
41+
> precondition for administration of entitlement given the logical _operators_ of the matched conditions and their relations.

e2e/subject-mapping.bats

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ teardown_file() {
4444
assert_output --partial '"Standard":1'
4545
assert_output --partial '"Standard":2'
4646
assert_output --partial ".team.name"
47-
assert_output --regexp "Attribute Value Id.*$VAL1_ID"
47+
assert_output --regexp "Attribute Value Id.*$VAL1_ID"
4848

4949
# scs is required
5050
run_otdfctl_sm create --attribute-value-id "$VAL2_ID" -s TRANSMIT
@@ -57,6 +57,40 @@ assert_output --regexp "Attribute Value Id.*$VAL1_ID"
5757
assert_output --partial "At least one Standard or Custom Action [--action-standard, --action-custom] is required"
5858
}
5959

60+
@test "Match subject mapping" {
61+
# create with simultaneous new SCS
62+
NEW_SCS='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["sales"],"subject_external_selector_value":".department"}],"boolean_operator":2}]}]'
63+
NEW_SM_ID=$(./otdfctl $HOST $WITH_CREDS policy subject-mappings create -a "$VAL2_ID" --action-standard DECRYPT --subject-condition-set-new "$NEW_SCS" --json | jq -r '.id')
64+
65+
run_otdfctl_sm match -x '.department'
66+
assert_success
67+
assert_output --partial "$NEW_SM_ID"
68+
69+
matched_subject='{"department":"any_department"}'
70+
run ./otdfctl policy sm match --subject "$matched_subject" $HOST $WITH_CREDS
71+
assert_success
72+
assert_output --partial "$NEW_SM_ID"
73+
74+
# JWT includes 'department' in token claims
75+
run_otdfctl_sm match -s 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50Ijoibm93aGVyZV9zcGVjaWFsIn0.784uXYtfOv4tdM6JRgBMua4bBNDjUGbcr89QQKzCXfU'
76+
assert_success
77+
assert_output --partial "$NEW_SM_ID"
78+
79+
run_otdfctl_sm match --selector '.not_found'
80+
assert_success
81+
refute_output --partial "$NEW_SM_ID"
82+
83+
unmatched_subject='{"dept":"nope"}'
84+
run ./otdfctl policy sm match -s "$unmatched_subject" $HOST $WITH_CREDS
85+
assert_success
86+
refute_output --partial "$NEW_SM_ID"
87+
88+
# JWT lacks 'department' in token claims
89+
run_otdfctl_sm match -s 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhYmMiOiJub3doZXJlX3NwZWNpYWwifQ.H39TXi1gYWRhXIRkfxFJwrZz42eE4y8V5BQX-mg8JAo'
90+
assert_success
91+
refute_output --partial "$NEW_SM_ID"
92+
}
93+
6094
@test "Get subject mapping" {
6195
new_scs=$(./otdfctl $HOST $WITH_CREDS policy scs create -s "$SCS_2" --json | jq -r '.id')
6296
created=$(./otdfctl $HOST $WITH_CREDS policy sm create -a "$VAL2_ID" -s TRANSMIT --subject-condition-set-id "$new_scs" --json | jq -r '.id')

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ require (
1313
github.com/evertras/bubble-table v0.16.1
1414
github.com/gabriel-vasile/mimetype v1.4.5
1515
github.com/go-jose/go-jose/v3 v3.0.3
16-
github.com/golang-jwt/jwt v3.2.2+incompatible
1716
github.com/google/uuid v1.6.0
18-
github.com/opentdf/platform/lib/flattening v0.1.1
1917
github.com/opentdf/platform/protocol/go v0.2.20
2018
github.com/opentdf/platform/sdk v0.3.19
2119
github.com/spf13/cobra v1.8.1
@@ -55,6 +53,7 @@ require (
5553
github.com/go-logr/stdr v1.2.2 // indirect
5654
github.com/goccy/go-json v0.10.3 // indirect
5755
github.com/godbus/dbus/v5 v5.1.0 // indirect
56+
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
5857
github.com/gorilla/css v1.0.0 // indirect
5958
github.com/gorilla/securecookie v1.1.2 // indirect
6059
github.com/gowebpki/jcs v1.0.1 // indirect
@@ -82,6 +81,7 @@ require (
8281
github.com/muesli/termenv v0.15.2 // indirect
8382
github.com/muhlemmer/gu v0.3.1 // indirect
8483
github.com/olekukonko/tablewriter v0.0.5 // indirect
84+
github.com/opentdf/platform/lib/flattening v0.1.1 // indirect
8585
github.com/opentdf/platform/lib/ocrypto v0.1.6 // indirect
8686
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
8787
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect

go.sum

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,8 @@ github.com/opentdf/platform/lib/flattening v0.1.1 h1:la1f6PcRsc+yLH8+9UEr0ux6IRK
221221
github.com/opentdf/platform/lib/flattening v0.1.1/go.mod h1:eyG7pe5UZlV+GI5/CymQD3xTAJxNhnP9M4QnBzaad1M=
222222
github.com/opentdf/platform/lib/ocrypto v0.1.6 h1:rd4ctCZOE/c3qDJORtkSK9tw6dEXb+jbJXRRk4LcxII=
223223
github.com/opentdf/platform/lib/ocrypto v0.1.6/go.mod h1:ne+l8Q922OdzA0xesK3XJmfECBnn5vLSGYU3/3OhiHM=
224-
github.com/opentdf/platform/protocol/go v0.2.18 h1:s+TVZkOPGCzy7WyObtJWJNaFeOGDUTuSmAsq3omvugY=
225-
github.com/opentdf/platform/protocol/go v0.2.18/go.mod h1:WqDcnFQJb0v8ivRQPidbehcL8ils5ZSZYXkuv0nyvsI=
226224
github.com/opentdf/platform/protocol/go v0.2.20 h1:FPU1ZcXvPm/QeE2nqgbD/HMTOCICQSD0DoncQbAZ1ws=
227225
github.com/opentdf/platform/protocol/go v0.2.20/go.mod h1:TWIuf387VeR3q0TL4nAMKQTWEqqID+8Yjao76EX9Dto=
228-
github.com/opentdf/platform/sdk v0.3.17 h1:Uo/kTMneB18i0gZNfTRtvw34bGLFUc8BEnA/BMK0VVs=
229-
github.com/opentdf/platform/sdk v0.3.17/go.mod h1:c2+nrsRLvLf2OOryXnNy0iGZN/TScc21Pul7uqKVXIs=
230226
github.com/opentdf/platform/sdk v0.3.18 h1:IY6fNrOfQD9lF/hZp9ewZsH0PMuLe17HlSE1A5kyIWc=
231227
github.com/opentdf/platform/sdk v0.3.18/go.mod h1:u+XZhVRsMq5blukCFCHcjk6HLCp4Y5mmIQu7GhtKQ3E=
232228
github.com/opentdf/platform/sdk v0.3.19 h1:4Ign6HPrxOH6ZllLO/cI6joSuqz8CqPlpxpTKunpMQs=

pkg/handlers/subjectmappings.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ func (h Handler) DeleteSubjectMapping(id string) (*policy.SubjectMapping, error)
6565
return resp.GetSubjectMapping(), err
6666
}
6767

68+
func (h Handler) MatchSubjectMappings(selectors []string) ([]*policy.SubjectMapping, error) {
69+
subjectProperties := make([]*policy.SubjectProperty, len(selectors))
70+
for i, selector := range selectors {
71+
subjectProperties[i] = &policy.SubjectProperty{
72+
ExternalSelectorValue: selector,
73+
}
74+
}
75+
resp, err := h.sdk.SubjectMapping.MatchSubjectMappings(h.ctx, &subjectmapping.MatchSubjectMappingsRequest{
76+
SubjectProperties: subjectProperties,
77+
})
78+
return resp.GetSubjectMappings(), err
79+
}
80+
6881
func GetSubjectMappingOperatorFromChoice(readable string) policy.SubjectMappingOperatorEnum {
6982
switch readable {
7083
case SubjectMappingOperatorIn:

0 commit comments

Comments
 (0)