diff --git a/docs/auth0_network-acl_create.md b/docs/auth0_network-acl_create.md index b5d86b042..59f472e87 100644 --- a/docs/auth0_network-acl_create.md +++ b/docs/auth0_network-acl_create.md @@ -26,8 +26,9 @@ auth0 network-acl create [flags] auth0 network-acl create --description "Complex Rule" --priority 5 --active true --rule '{"action":{"block":true},"scope":"tenant","match":{"ipv4_cidrs":["192.168.1.0/24"],"geo_country_codes":["US"]}}' auth0 network-acl create --description "Deny All" --priority 7 --active true --rule '{"action":{"block":true},"scope":"tenant","match_all":true}' - # Early Access (auth0_managed match/not_match value): + # Early Access (auth0_managed and http_message_signature match/not_match value): auth0 network-acl create -d "Curated Blocklist" -p 6 --active true --rule '{"action":{"log":true},"scope":"tenant","not_match":{"auth0_managed":["auth0.vpn","auth0.proxy"]}}' + auth0 network-acl create -d "Only Signed" -p 8 --active true --rule '{"action":{"allow":true},"scope":"authentication","match":{"http_message_signature":{"keys":[{"id": "key_123"}]}}}' ``` diff --git a/docs/auth0_network-acl_update.md b/docs/auth0_network-acl_update.md index accdde74a..f9b070dab 100644 --- a/docs/auth0_network-acl_update.md +++ b/docs/auth0_network-acl_update.md @@ -26,8 +26,9 @@ auth0 network-acl update [flags] auth0 network-acl update --description "Complex Rule updated" --priority 1 --active true --rule '{"action":{"block":true},"scope":"tenant","match":{"ipv4_cidrs":["192.168.1.0/24"],"geo_country_codes":["US"]}}' auth0 network-acl update --rule '{"action":{"block":true},"scope":"tenant","match_all":true}' - # Early Access (auth0_managed match/not_match value): + # Early Access (auth0_managed and http_message_signature match/not_match value): auth0 network-acl update --rule '{"action":{"allow":true},"scope":"tenant","match":{"auth0_managed":["auth0.low_reputation"]}}' + auth0 network-acl update --rule '{"action":{"allow":true},"scope":"authentication","match":{"http_message_signature":{"keys":[{"id": "key_123"},{"id": "key_456"}]}}}' ``` diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 1ec8df22f..61dcc60c2 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -89,6 +89,7 @@ type APIV3 struct { UserRefreshToken UserRefreshTokenAPIV3 ActionModule ActionModuleAPIV3 ActionModuleVersion ActionModuleVersionAPIV3 + NetworkACLKey NetworkACLKeyAPIV3 } func NewAPIV3(m *managementv3.Management) *APIV3 { @@ -108,6 +109,7 @@ func NewAPIV3(m *managementv3.Management) *APIV3 { UserRefreshToken: m.Users.RefreshToken, ActionModule: m.Actions.Modules, ActionModuleVersion: m.Actions.Modules.Versions, + NetworkACLKey: m.Keys.NetworkACLs, } } diff --git a/internal/auth0/mock/network_acl_key_mock.go b/internal/auth0/mock/network_acl_key_mock.go new file mode 100644 index 000000000..f42502f7b --- /dev/null +++ b/internal/auth0/mock/network_acl_key_mock.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: network_acl_key.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + management "github.com/auth0/go-auth0/v3/management" + option "github.com/auth0/go-auth0/v3/management/option" + gomock "github.com/golang/mock/gomock" +) + +// MockNetworkACLKeyAPIV3 is a mock of NetworkACLKeyAPIV3 interface. +type MockNetworkACLKeyAPIV3 struct { + ctrl *gomock.Controller + recorder *MockNetworkACLKeyAPIV3MockRecorder +} + +// MockNetworkACLKeyAPIV3MockRecorder is the mock recorder for MockNetworkACLKeyAPIV3. +type MockNetworkACLKeyAPIV3MockRecorder struct { + mock *MockNetworkACLKeyAPIV3 +} + +// NewMockNetworkACLKeyAPIV3 creates a new mock instance. +func NewMockNetworkACLKeyAPIV3(ctrl *gomock.Controller) *MockNetworkACLKeyAPIV3 { + mock := &MockNetworkACLKeyAPIV3{ctrl: ctrl} + mock.recorder = &MockNetworkACLKeyAPIV3MockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNetworkACLKeyAPIV3) EXPECT() *MockNetworkACLKeyAPIV3MockRecorder { + return m.recorder +} + +// List mocks base method. +func (m *MockNetworkACLKeyAPIV3) List(ctx context.Context, opts ...option.RequestOption) (*management.GetAllKeysNetworkACLsResponseContent, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(*management.GetAllKeysNetworkACLsResponseContent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockNetworkACLKeyAPIV3MockRecorder) List(ctx interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNetworkACLKeyAPIV3)(nil).List), varargs...) +} diff --git a/internal/auth0/network_acl_key.go b/internal/auth0/network_acl_key.go new file mode 100644 index 000000000..78d7b74b1 --- /dev/null +++ b/internal/auth0/network_acl_key.go @@ -0,0 +1,22 @@ +//go:generate mockgen -source=network_acl_key.go -destination=mock/network_acl_key_mock.go -package=mock + +package auth0 + +import ( + "context" + + managementv3 "github.com/auth0/go-auth0/v3/management" + "github.com/auth0/go-auth0/v3/management/option" +) + +// NetworkACLKeyAPIV3 is the V3 SDK interface for the /keys/network-acls endpoint. +// +// Only List is exposed today: the network-acl command uses it to let a user pick +// existing signing keys by name when adding the http_message_signature signal to a +// rule. Key create/delete is intentionally not wired yet (DXCDT-2269). +type NetworkACLKeyAPIV3 interface { + // List retrieves all Network ACL keys for the tenant. + // + // Required scope: `read:network_acl_keys`. The response is not paginated. + List(ctx context.Context, opts ...option.RequestOption) (*managementv3.GetAllKeysNetworkACLsResponseContent, error) +} diff --git a/internal/cli/network_acl.go b/internal/cli/network_acl.go index 357331d72..0cfc8ecbe 100644 --- a/internal/cli/network_acl.go +++ b/internal/cli/network_acl.go @@ -201,6 +201,7 @@ func selectNetworkACLParams() (map[string]bool, error) { "JA4Fingerprints", "User Agents", "Auth0 Managed", + "Signature Keys", } var selected []string @@ -228,22 +229,23 @@ func selectNetworkACLParams() (map[string]bool, error) { // ruleDefaults holds default values extracted from current ACL rule. type ruleDefaults struct { - Scope string - Action string - RedirectURI string - ASNs []int - CountryCodes []string - SubdivCodes []string - IPv4CIDRs []string - IPv6CIDRs []string - JA3 []string - JA4 []string - UserAgents []string - Auth0Managed []string - IsMatchRule bool - HasMatchRule bool - HasNotMatch bool - MatchAll bool + Scope string + Action string + RedirectURI string + ASNs []int + CountryCodes []string + SubdivCodes []string + IPv4CIDRs []string + IPv6CIDRs []string + JA3 []string + JA4 []string + UserAgents []string + Auth0Managed []string + SignatureKeyIDs []string + IsMatchRule bool + HasMatchRule bool + HasNotMatch bool + MatchAll bool } // extractCurrentRuleDefaults extracts default values from current ACL rule for interactive prompts. @@ -322,29 +324,46 @@ func extractCurrentRuleDefaults(currentACL *management.NetworkACL) *ruleDefaults if match.Auth0Managed != nil { defaults.Auth0Managed = *match.Auth0Managed } + defaults.SignatureKeyIDs = signatureKeyIDs(match) } return defaults } +// signatureKeyIDs flattens the referenced key ids of a match's +// http_message_signature signal, or nil when the signal is not set. +func signatureKeyIDs(match *management.NetworkACLRuleMatch) []string { + if match == nil || match.HTTPMessageSignature == nil { + return nil + } + ids := make([]string, 0, len(match.HTTPMessageSignature.Keys)) + for _, k := range match.HTTPMessageSignature.Keys { + if k.ID != nil { + ids = append(ids, *k.ID) + } + } + return ids +} + // ruleInputs holds user inputs for rule configuration. type ruleInputs struct { - Scope string - Action string - RedirectURI string - ASNs []int - CountryCodes []string - SubdivCodes []string - IPv4CIDRs []string - IPv6CIDRs []string - JA3 []string - JA4 []string - UserAgents []string - Auth0Managed []string - IsMatchRule bool - MatchRule bool - NoMatchRule bool - MatchAll bool + Scope string + Action string + RedirectURI string + ASNs []int + CountryCodes []string + SubdivCodes []string + IPv4CIDRs []string + IPv6CIDRs []string + JA3 []string + JA4 []string + UserAgents []string + Auth0Managed []string + SignatureKeyIDs []string + IsMatchRule bool + MatchRule bool + NoMatchRule bool + MatchAll bool } // promptForRuleDetails handles interactive prompting for rule configuration. @@ -415,7 +434,7 @@ func promptForRuleDetails(cmd *cobra.Command, cli *cli, defaults *ruleDefaults, var selectedMatchOption string if err := (&Flag{ Name: "What kind of rule do you want to create?", - Help: "Match or Not Match rule (ASNs, Country Codes, Subdivision Codes, IPv4 CIDRs, IPv6 CIDRs, JA3/JA4 Fingerprints, User Agents, Auth0 Managed)", + Help: "Match or Not Match rule (ASNs, Country Codes, Subdivision Codes, IPv4 CIDRs, IPv6 CIDRs, JA3/JA4 Fingerprints, User Agents, Auth0 Managed, Signature Keys)", }).Select(cmd, &selectedMatchOption, matchOptions, nil); err != nil { return nil, err } @@ -429,7 +448,7 @@ func promptForRuleDetails(cmd *cobra.Command, cli *cli, defaults *ruleDefaults, } // Ask for values only for selected parameters. - if err := promptForMatchCriteria(cmd, selectedParams, inputs, defaults); err != nil { + if err := promptForMatchCriteria(cmd, cli, selectedParams, inputs, defaults); err != nil { return nil, err } @@ -437,7 +456,7 @@ func promptForRuleDetails(cmd *cobra.Command, cli *cli, defaults *ruleDefaults, } // promptForMatchCriteria handles prompting for all match criteria based on selected parameters. -func promptForMatchCriteria(cmd *cobra.Command, selectedParams map[string]bool, inputs *ruleInputs, defaults *ruleDefaults) error { +func promptForMatchCriteria(cmd *cobra.Command, cli *cli, selectedParams map[string]bool, inputs *ruleInputs, defaults *ruleDefaults) error { if selectedParams["ASNs"] { if err := (&Flag{ Name: "ASNs", @@ -527,9 +546,74 @@ func promptForMatchCriteria(cmd *cobra.Command, selectedParams map[string]bool, } } + if selectedParams["Signature Keys"] { + ids, err := cli.pickNetworkACLSignatureKeys(cmd, defaults.SignatureKeyIDs) + if err != nil { + return err + } + inputs.SignatureKeyIDs = ids + } + return nil } +// pickNetworkACLSignatureKeys resolves the http_message_signature key ids for a rule. +// +// It lists the tenant's Network ACL keys (v3 /keys/network-acls) and shows a multi-select +// of them, with the ids already referenced by the rule (current) pre-selected. Setting the +// signal non-interactively is done through the full rule JSON passed to --rule. +func (c *cli) pickNetworkACLSignatureKeys(cmd *cobra.Command, current []string) ([]string, error) { + var options []string + labelToID := make(map[string]string) + idToLabel := make(map[string]string) + if err := ansi.Waiting(func() error { + resp, err := c.apiv3.NetworkACLKey.List(cmd.Context()) + if err != nil { + return err + } + if resp != nil { + for _, k := range resp.Keys { + id := k.GetID() + label := fmt.Sprintf("%s (%s)", k.GetName(), id) + options = append(options, label) + idToLabel[id] = label + labelToID[label] = id + } + } + return nil + }); err != nil { + return nil, err + } + + if len(options) == 0 { + return nil, errors.New("no Network ACL keys exist for this tenant; create a key first, then reference it here") + } + + // Pre-select the keys already referenced by the rule. + defaults := make([]string, 0, len(current)) + for _, id := range current { + if label, ok := idToLabel[id]; ok { + defaults = append(defaults, label) + } + } + + var selected []string + if err := prompt.AskMultiSelectWithDefault( + "Select the signing keys whose HTTP message signature satisfies the rule using the spacebar and press Enter to confirm:", + &selected, + defaults, + options..., + ); err != nil { + return nil, err + } + + ids := make([]string, 0, len(selected)) + for _, label := range selected { + ids = append(ids, labelToID[label]) + } + return ids, nil +} + // buildNetworkACLRule creates a NetworkACLRule from the provided inputs. func buildNetworkACLRule(inputs *ruleInputs) (*management.NetworkACLRule, error) { rule := &management.NetworkACLRule{ @@ -595,6 +679,14 @@ func buildNetworkACLRule(inputs *ruleInputs) (*management.NetworkACLRule, error) match.Auth0Managed = &inputs.Auth0Managed matchProvided = true } + if len(inputs.SignatureKeyIDs) > 0 { + keys := make([]*management.NetworkACLHTTPMessageSignatureKey, len(inputs.SignatureKeyIDs)) + for i := range inputs.SignatureKeyIDs { + keys[i] = &management.NetworkACLHTTPMessageSignatureKey{ID: &inputs.SignatureKeyIDs[i]} + } + match.HTTPMessageSignature = &management.NetworkACLHTTPMessageSignature{Keys: keys} + matchProvided = true + } if !matchProvided { return nil, fmt.Errorf("at least one match criteria must be provided") @@ -715,8 +807,9 @@ The --rule parameter is required and must contain a valid JSON object with actio auth0 network-acl create --description "Complex Rule" --priority 5 --active true --rule '{"action":{"block":true},"scope":"tenant","match":{"ipv4_cidrs":["192.168.1.0/24"],"geo_country_codes":["US"]}}' auth0 network-acl create --description "Deny All" --priority 7 --active true --rule '{"action":{"block":true},"scope":"tenant","match_all":true}' - # Early Access (auth0_managed match/not_match value): + # Early Access (auth0_managed and http_message_signature match/not_match value): auth0 network-acl create -d "Curated Blocklist" -p 6 --active true --rule '{"action":{"log":true},"scope":"tenant","not_match":{"auth0_managed":["auth0.vpn","auth0.proxy"]}}' + auth0 network-acl create -d "Only Signed" -p 8 --active true --rule '{"action":{"allow":true},"scope":"authentication","match":{"http_message_signature":{"keys":[{"id": "key_123"}]}}}' `, RunE: func(cmd *cobra.Command, args []string) error { // Validate --rule JSON up front, before prompting for other fields, so @@ -818,8 +911,9 @@ To update non-interactively, supply the description, active, priority, and rule auth0 network-acl update --description "Complex Rule updated" --priority 1 --active true --rule '{"action":{"block":true},"scope":"tenant","match":{"ipv4_cidrs":["192.168.1.0/24"],"geo_country_codes":["US"]}}' auth0 network-acl update --rule '{"action":{"block":true},"scope":"tenant","match_all":true}' - # Early Access (auth0_managed match/not_match value): + # Early Access (auth0_managed and http_message_signature match/not_match value): auth0 network-acl update --rule '{"action":{"allow":true},"scope":"tenant","match":{"auth0_managed":["auth0.low_reputation"]}}' + auth0 network-acl update --rule '{"action":{"allow":true},"scope":"authentication","match":{"http_message_signature":{"keys":[{"id": "key_123"},{"id": "key_456"}]}}}' `, RunE: func(cmd *cobra.Command, args []string) error { // Get the network ACL ID. diff --git a/internal/cli/network_acl_test.go b/internal/cli/network_acl_test.go index 388de17ad..e30bc40f3 100644 --- a/internal/cli/network_acl_test.go +++ b/internal/cli/network_acl_test.go @@ -285,6 +285,146 @@ func TestExtractCurrentRuleDefaults_MatchAll(t *testing.T) { } } +func TestBuildNetworkACLRule_HTTPMessageSignature(t *testing.T) { + tests := []struct { + name string + inputs *ruleInputs + assertRule func(t testing.TB, rule *management.NetworkACLRule) + expectError bool + }{ + { + name: "http_message_signature on match", + inputs: &ruleInputs{ + Scope: "authentication", + Action: "block", + SignatureKeyIDs: []string{"key_123", "key_456"}, + IsMatchRule: true, + }, + assertRule: func(t testing.TB, rule *management.NetworkACLRule) { + assert.Nil(t, rule.NotMatch) + assert.NotNil(t, rule.Match) + assert.NotNil(t, rule.Match.HTTPMessageSignature) + assert.Len(t, rule.Match.HTTPMessageSignature.Keys, 2) + assert.Equal(t, "key_123", *rule.Match.HTTPMessageSignature.Keys[0].ID) + assert.Equal(t, "key_456", *rule.Match.HTTPMessageSignature.Keys[1].ID) + }, + }, + { + name: "http_message_signature on not_match", + inputs: &ruleInputs{ + Scope: "authentication", + Action: "block", + SignatureKeyIDs: []string{"key_123"}, + IsMatchRule: false, + }, + assertRule: func(t testing.TB, rule *management.NetworkACLRule) { + assert.Nil(t, rule.Match) + assert.NotNil(t, rule.NotMatch) + assert.NotNil(t, rule.NotMatch.HTTPMessageSignature) + assert.Len(t, rule.NotMatch.HTTPMessageSignature.Keys, 1) + assert.Equal(t, "key_123", *rule.NotMatch.HTTPMessageSignature.Keys[0].ID) + }, + }, + { + name: "http_message_signature coexists with other criteria", + inputs: &ruleInputs{ + Scope: "authentication", + Action: "block", + IPv4CIDRs: []string{"192.168.1.0/24"}, + SignatureKeyIDs: []string{"key_123"}, + IsMatchRule: true, + }, + assertRule: func(t testing.TB, rule *management.NetworkACLRule) { + assert.NotNil(t, rule.Match) + assert.NotNil(t, rule.Match.IPv4Cidrs) + assert.NotNil(t, rule.Match.HTTPMessageSignature) + assert.Len(t, rule.Match.HTTPMessageSignature.Keys, 1) + }, + }, + { + name: "http_message_signature empty is not set", + inputs: &ruleInputs{ + Scope: "authentication", + Action: "block", + IsMatchRule: true, + }, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rule, err := buildNetworkACLRule(test.inputs) + + if test.expectError { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + test.assertRule(t, rule) + }) + } +} + +func TestExtractCurrentRuleDefaults_HTTPMessageSignature(t *testing.T) { + tests := []struct { + name string + acl *management.NetworkACL + wantKeys []string + }{ + { + name: "extracts signature key ids from match", + acl: &management.NetworkACL{ + Rule: &management.NetworkACLRule{ + Match: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: auth0.String("key_123")}, + {ID: auth0.String("key_456")}, + }, + }, + }, + }, + }, + wantKeys: []string{"key_123", "key_456"}, + }, + { + name: "extracts signature key ids from not_match", + acl: &management.NetworkACL{ + Rule: &management.NetworkACLRule{ + NotMatch: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: auth0.String("key_123")}, + }, + }, + }, + }, + }, + wantKeys: []string{"key_123"}, + }, + { + name: "no signature keys set", + acl: &management.NetworkACL{ + Rule: &management.NetworkACLRule{ + Match: &management.NetworkACLRuleMatch{ + IPv4Cidrs: &[]string{"192.168.1.0/24"}, + }, + }, + }, + wantKeys: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + defaults := extractCurrentRuleDefaults(test.acl) + assert.Equal(t, test.wantKeys, defaults.SignatureKeyIDs) + }) + } +} + func TestExtractCurrentRuleDefaults_Auth0Managed(t *testing.T) { tests := []struct { name string diff --git a/internal/display/network_acl.go b/internal/display/network_acl.go index a63344713..c50e78fe0 100644 --- a/internal/display/network_acl.go +++ b/internal/display/network_acl.go @@ -109,6 +109,10 @@ func (v *networkACLView) KeyValues() [][]string { if match.Auth0Managed != nil && len(*match.Auth0Managed) > 0 { keyValues = append(keyValues, []string{"AUTH0 MANAGED", strings.Join(*match.Auth0Managed, ", ")}) } + + if ids := httpMessageSignatureKeyIDs(match); len(ids) > 0 { + keyValues = append(keyValues, []string{"SIGNATURE KEY IDS", strings.Join(ids, ", ")}) + } } // Add not_match criteria if present. @@ -154,12 +158,33 @@ func (v *networkACLView) KeyValues() [][]string { if notMatch.Auth0Managed != nil && len(*notMatch.Auth0Managed) > 0 { keyValues = append(keyValues, []string{"NOT AUTH0 MANAGED", strings.Join(*notMatch.Auth0Managed, ", ")}) } + + if ids := httpMessageSignatureKeyIDs(notMatch); len(ids) > 0 { + keyValues = append(keyValues, []string{"NOT SIGNATURE KEY IDS", strings.Join(ids, ", ")}) + } } } return keyValues } +// httpMessageSignatureKeyIDs returns the signing key IDs referenced by a rule's +// http_message_signature signal, or nil when the signal is absent. +func httpMessageSignatureKeyIDs(match *management.NetworkACLRuleMatch) []string { + if match == nil || match.HTTPMessageSignature == nil { + return nil + } + + ids := make([]string, 0, len(match.HTTPMessageSignature.Keys)) + for _, k := range match.HTTPMessageSignature.Keys { + if k.ID != nil { + ids = append(ids, *k.ID) + } + } + + return ids +} + func (v *networkACLView) Object() interface{} { return v.raw } diff --git a/internal/display/network_acl_test.go b/internal/display/network_acl_test.go index 89ac1be51..d6948a808 100644 --- a/internal/display/network_acl_test.go +++ b/internal/display/network_acl_test.go @@ -100,6 +100,72 @@ func TestNetworkACLView_KeyValues_MatchAll(t *testing.T) { assert.Equal(t, "true", value) } +func TestNetworkACLView_KeyValues_HTTPMessageSignature(t *testing.T) { + tests := []struct { + name string + acl *management.NetworkACL + wantKey string + wantValue string + }{ + { + name: "http_message_signature on match", + acl: &management.NetworkACL{ + ID: strPtr("acl-1"), + Description: strPtr("Only Signed"), + Priority: intPtr(1), + Active: boolPtr(true), + Rule: &management.NetworkACLRule{ + Scope: strPtr("authentication"), + Action: &management.NetworkACLRuleAction{Block: boolPtr(true)}, + Match: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: strPtr("key_123")}, + {ID: strPtr("key_456")}, + }, + }, + }, + }, + }, + wantKey: "SIGNATURE KEY IDS", + wantValue: "key_123, key_456", + }, + { + name: "http_message_signature on not_match", + acl: &management.NetworkACL{ + ID: strPtr("acl-2"), + Description: strPtr("Reject Signed"), + Priority: intPtr(2), + Active: boolPtr(true), + Rule: &management.NetworkACLRule{ + Scope: strPtr("authentication"), + Action: &management.NetworkACLRuleAction{Block: boolPtr(true)}, + NotMatch: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: strPtr("key_123")}, + }, + }, + }, + }, + }, + wantKey: "NOT SIGNATURE KEY IDS", + wantValue: "key_123", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + view := makeNetworkACLView(test.acl) + kvs := view.KeyValues() + + value, ok := keyValue(kvs, test.wantKey) + assert.True(t, ok, "expected key %q to be present in KeyValues()", test.wantKey) + assert.Equal(t, test.wantValue, value) + }) + } +} + // TestNetworkACLView_Object_IncludesID guards against a regression where storing // a *management.NetworkACL in the view's raw field engaged that type's pointer // receiver MarshalJSON, which emits only the writable subset of fields and drops diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index 1cc8e53ab..cbd7a85c2 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -35,6 +35,17 @@ func AskMultiSelect(message string, response interface{}, options ...string) err return err } +// AskMultiSelectWithDefault is AskMultiSelect with the given default options pre-selected. +func AskMultiSelectWithDefault(message string, response interface{}, defaults []string, options ...string) error { + prompt := &survey.MultiSelect{ + Message: message, + Options: options, + Default: defaults, + } + + return askOne(prompt, response) +} + func AskBool(message string, value *bool, defaultValue bool) error { *value = defaultValue prompt := &survey.Confirm{