Skip to content

Commit

Permalink
feat(core): add subject-mappings match to CLI (#413)
Browse files Browse the repository at this point in the history
Unblocked by the merge of opentdf/platform#1658
Closes #410
  • Loading branch information
jakedoublev authored Nov 19, 2024
1 parent 79f2079 commit bc56c19
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 10 deletions.
1 change: 1 addition & 0 deletions .github/spellcheck.ignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ SCS
SCSs
SDK
ShinyThing
SubjectConditionSets
TDF
TDF'd
TDFd
Expand Down
83 changes: 83 additions & 0 deletions cmd/policy-subjectMappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/evertras/bubble-table/table"
"github.com/opentdf/otdfctl/pkg/cli"
"github.com/opentdf/otdfctl/pkg/handlers"
"github.com/opentdf/otdfctl/pkg/man"
"github.com/opentdf/platform/protocol/go/policy"
"github.com/opentdf/platform/protocol/go/policy/subjectmapping"
Expand Down Expand Up @@ -253,6 +254,70 @@ func policy_updateSubjectMapping(cmd *cobra.Command, args []string) {
HandleSuccess(cmd, id, t, updated)
}

func policy_matchSubjectMappings(cmd *cobra.Command, args []string) {
c := cli.New(cmd, args)
h := NewHandler(c)
defer h.Close()

subject := c.Flags.GetOptionalString("subject")
selectors = c.Flags.GetStringSlice("selector", selectors, cli.FlagsStringSliceOptions{Min: 0})

if len(selectors) > 0 && subject != "" {
cli.ExitWithError("Must provide either '--subject' or '--selector' flag values, not both", nil)
}

if subject != "" {
flattened, err := handlers.FlattenSubjectContext(subject)
if err != nil {
cli.ExitWithError("Could not process '--subject' value", err)
}
for _, item := range flattened {
selectors = append(selectors, item.Key)
}
}

matched, err := h.MatchSubjectMappings(selectors)
if err != nil {
cli.ExitWithError(fmt.Sprintf("Failed to match subject mappings with selectors %v", selectors), err)
}

t := cli.NewTable(
cli.NewUUIDColumn(),
table.NewFlexColumn("subject_attrval_id", "Subject AttrVal: Id", cli.FlexColumnWidthFour),
table.NewFlexColumn("subject_attrval_value", "Subject AttrVal: Value", cli.FlexColumnWidthThree),
table.NewFlexColumn("actions", "Actions", cli.FlexColumnWidthTwo),
table.NewFlexColumn("subject_condition_set_id", "Subject Condition Set: Id", cli.FlexColumnWidthFour),
table.NewFlexColumn("subject_condition_set", "Subject Condition Set", cli.FlexColumnWidthThree),
)
rows := []table.Row{}
for _, sm := range matched {
var actionsJSON []byte
if actionsJSON, err = json.Marshal(sm.GetActions()); err != nil {
cli.ExitWithError("Error marshalling subject mapping actions", err)
}

var subjectSetsJSON []byte
if subjectSetsJSON, err = json.Marshal(sm.GetSubjectConditionSet().GetSubjectSets()); err != nil {
cli.ExitWithError("Error marshalling subject condition set", err)
}
metadata := cli.ConstructMetadata(sm.GetMetadata())

rows = append(rows, table.NewRow(table.RowData{
"id": sm.GetId(),
"subject_attrval_id": sm.GetAttributeValue().GetId(),
"subject_attrval_value": sm.GetAttributeValue().GetValue(),
"actions": string(actionsJSON),
"subject_condition_set_id": sm.GetSubjectConditionSet().GetId(),
"subject_condition_set": string(subjectSetsJSON),
"labels": metadata["Labels"],
"created_at": metadata["Created At"],
"updated_at": metadata["Updated At"],
}))
}
t = t.WithRows(rows)
HandleSuccess(cmd, "", t, matched)
}

func getSubjectMappingMappingActionEnumFromChoice(readable string) policy.Action_StandardAction {
switch readable {
case actionStandardDecrypt:
Expand Down Expand Up @@ -378,13 +443,31 @@ func init() {
deleteDoc.GetDocFlag("force").Description,
)

matchDoc := man.Docs.GetCommand("policy/subject-mappings/match",
man.WithRun(policy_matchSubjectMappings),
)
matchDoc.Flags().StringP(
matchDoc.GetDocFlag("subject").Name,
matchDoc.GetDocFlag("subject").Shorthand,
matchDoc.GetDocFlag("subject").Default,
matchDoc.GetDocFlag("subject").Description,
)
matchDoc.Flags().StringSliceVarP(
&selectors,
matchDoc.GetDocFlag("selector").Name,
matchDoc.GetDocFlag("selector").Shorthand,
[]string{},
matchDoc.GetDocFlag("selector").Description,
)

doc := man.Docs.GetCommand("policy/subject-mappings",
man.WithSubcommands(
createDoc,
getDoc,
listDoc,
updateDoc,
deleteDoc,
matchDoc,
),
)
policy_subjectMappingCmd := &doc.Command
Expand Down
4 changes: 2 additions & 2 deletions docs/man/auth/client-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ in the OS keyring for future use.
Authenticate with client credentials (secret provided interactively)

```shell
opentdf auth client-credentials --client-id <client-id>
otdfctl auth client-credentials --client-id <client-id>
```

Authenticate with client credentials (secret provided as argument)

```shell
opentdf auth client-credentials --client-id <client-id> --client-secret <client-secret>
otdfctl auth client-credentials --client-id <client-id> --client-secret <client-secret>
```
2 changes: 1 addition & 1 deletion docs/man/interactive.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Interactive Mode
title: Interactive Mode (experimental)

command:
name: interactive
Expand Down
41 changes: 41 additions & 0 deletions docs/man/policy/subject-mappings/match.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Match a subject or set of selectors to relevant subject mappings
command:
name: match
flags:
- name: subject
shorthand: s
description: A Subject Entity Representation string (JSON or JWT, auto-detected)
default: ''
- name: selector
shorthand: x
description: "Individual selectors (i.e. '.department' or '.realm_access.roles[]') that may be found in SubjectConditionSets"
---

This tool queries platform policies for relevant Subject Mappings using either an Entity Representation or specific selectors.

If an Entity Representation is provided via `--subject` (such as an OIDC JWT or JSON response from an Entity Resolution Service), the tool
parses all valid selectors and checks for matching Subject Condition Sets in Subject Mappings to Attribute Values.

If selectors are provided directly with `--selector`, the tool searches for Subject Mappings with Subject Condition Sets that contain those selectors.

## Examples

Various ways to invoke the `match` command to query Subject Mappings to Attribute Values with relevant Subject Condition Sets.

```shell
# matches either org name or department selectors
otdfctl policy subject-mappings match --selector '.org.name' --selector '.department'

# parses subject entity representation as JSON and matches any selector (with this subject only '.emailAddress')
otdfctl policy subject-mappings match --subject '{"emailAddress":"[email protected]"}'

# parses entity representation as JWT into all possicle claim selectors and matches any of them
otdfctl policy subject-mappings match --subject 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
```

> [!NOTE]
> The values of the selectors and any `IN`/`NOT_IN`/`IN_CONTAINS` logic of Subject Condition Sets is irrelevant to this command.
> Evaluation of any matched conditions is handled by the Authorization Service to determine entitlements. This command
> is specifically for management of policy - to facilitate lookup of current conditions driven by known selectors as a
> precondition for administration of entitlement given the logical _operators_ of the matched conditions and their relations.
36 changes: 35 additions & 1 deletion e2e/subject-mapping.bats
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ teardown_file() {
assert_output --partial '"Standard":1'
assert_output --partial '"Standard":2'
assert_output --partial ".team.name"
assert_output --regexp "Attribute Value Id.*$VAL1_ID"
assert_output --regexp "Attribute Value Id.*$VAL1_ID"

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

@test "Match subject mapping" {
# create with simultaneous new SCS
NEW_SCS='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["sales"],"subject_external_selector_value":".department"}],"boolean_operator":2}]}]'
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')

run_otdfctl_sm match -x '.department'
assert_success
assert_output --partial "$NEW_SM_ID"

matched_subject='{"department":"any_department"}'
run ./otdfctl policy sm match --subject "$matched_subject" $HOST $WITH_CREDS
assert_success
assert_output --partial "$NEW_SM_ID"

# JWT includes 'department' in token claims
run_otdfctl_sm match -s 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50Ijoibm93aGVyZV9zcGVjaWFsIn0.784uXYtfOv4tdM6JRgBMua4bBNDjUGbcr89QQKzCXfU'
assert_success
assert_output --partial "$NEW_SM_ID"

run_otdfctl_sm match --selector '.not_found'
assert_success
refute_output --partial "$NEW_SM_ID"

unmatched_subject='{"dept":"nope"}'
run ./otdfctl policy sm match -s "$unmatched_subject" $HOST $WITH_CREDS
assert_success
refute_output --partial "$NEW_SM_ID"

# JWT lacks 'department' in token claims
run_otdfctl_sm match -s 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhYmMiOiJub3doZXJlX3NwZWNpYWwifQ.H39TXi1gYWRhXIRkfxFJwrZz42eE4y8V5BQX-mg8JAo'
assert_success
refute_output --partial "$NEW_SM_ID"
}

@test "Get subject mapping" {
new_scs=$(./otdfctl $HOST $WITH_CREDS policy scs create -s "$SCS_2" --json | jq -r '.id')
created=$(./otdfctl $HOST $WITH_CREDS policy sm create -a "$VAL2_ID" -s TRANSMIT --subject-condition-set-id "$new_scs" --json | jq -r '.id')
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ require (
github.com/evertras/bubble-table v0.16.1
github.com/gabriel-vasile/mimetype v1.4.5
github.com/go-jose/go-jose/v3 v3.0.3
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.6.0
github.com/opentdf/platform/lib/flattening v0.1.1
github.com/opentdf/platform/protocol/go v0.2.20
github.com/opentdf/platform/sdk v0.3.19
github.com/spf13/cobra v1.8.1
Expand Down Expand Up @@ -55,6 +53,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gowebpki/jcs v1.0.1 // indirect
Expand Down Expand Up @@ -82,6 +81,7 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opentdf/platform/lib/flattening v0.1.1 // indirect
github.com/opentdf/platform/lib/ocrypto v0.1.6 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,8 @@ github.com/opentdf/platform/lib/flattening v0.1.1 h1:la1f6PcRsc+yLH8+9UEr0ux6IRK
github.com/opentdf/platform/lib/flattening v0.1.1/go.mod h1:eyG7pe5UZlV+GI5/CymQD3xTAJxNhnP9M4QnBzaad1M=
github.com/opentdf/platform/lib/ocrypto v0.1.6 h1:rd4ctCZOE/c3qDJORtkSK9tw6dEXb+jbJXRRk4LcxII=
github.com/opentdf/platform/lib/ocrypto v0.1.6/go.mod h1:ne+l8Q922OdzA0xesK3XJmfECBnn5vLSGYU3/3OhiHM=
github.com/opentdf/platform/protocol/go v0.2.18 h1:s+TVZkOPGCzy7WyObtJWJNaFeOGDUTuSmAsq3omvugY=
github.com/opentdf/platform/protocol/go v0.2.18/go.mod h1:WqDcnFQJb0v8ivRQPidbehcL8ils5ZSZYXkuv0nyvsI=
github.com/opentdf/platform/protocol/go v0.2.20 h1:FPU1ZcXvPm/QeE2nqgbD/HMTOCICQSD0DoncQbAZ1ws=
github.com/opentdf/platform/protocol/go v0.2.20/go.mod h1:TWIuf387VeR3q0TL4nAMKQTWEqqID+8Yjao76EX9Dto=
github.com/opentdf/platform/sdk v0.3.17 h1:Uo/kTMneB18i0gZNfTRtvw34bGLFUc8BEnA/BMK0VVs=
github.com/opentdf/platform/sdk v0.3.17/go.mod h1:c2+nrsRLvLf2OOryXnNy0iGZN/TScc21Pul7uqKVXIs=
github.com/opentdf/platform/sdk v0.3.18 h1:IY6fNrOfQD9lF/hZp9ewZsH0PMuLe17HlSE1A5kyIWc=
github.com/opentdf/platform/sdk v0.3.18/go.mod h1:u+XZhVRsMq5blukCFCHcjk6HLCp4Y5mmIQu7GhtKQ3E=
github.com/opentdf/platform/sdk v0.3.19 h1:4Ign6HPrxOH6ZllLO/cI6joSuqz8CqPlpxpTKunpMQs=
Expand Down
13 changes: 13 additions & 0 deletions pkg/handlers/subjectmappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ func (h Handler) DeleteSubjectMapping(id string) (*policy.SubjectMapping, error)
return resp.GetSubjectMapping(), err
}

func (h Handler) MatchSubjectMappings(selectors []string) ([]*policy.SubjectMapping, error) {
subjectProperties := make([]*policy.SubjectProperty, len(selectors))
for i, selector := range selectors {
subjectProperties[i] = &policy.SubjectProperty{
ExternalSelectorValue: selector,
}
}
resp, err := h.sdk.SubjectMapping.MatchSubjectMappings(h.ctx, &subjectmapping.MatchSubjectMappingsRequest{
SubjectProperties: subjectProperties,
})
return resp.GetSubjectMappings(), err
}

func GetSubjectMappingOperatorFromChoice(readable string) policy.SubjectMappingOperatorEnum {
switch readable {
case SubjectMappingOperatorIn:
Expand Down

0 comments on commit bc56c19

Please sign in to comment.