diff --git a/docs/auth0_network-acl_create.md b/docs/auth0_network-acl_create.md index 234eaa583..f2c41507f 100644 --- a/docs/auth0_network-acl_create.md +++ b/docs/auth0_network-acl_create.md @@ -25,8 +25,9 @@ auth0 network-acl create [flags] auth0 network-acl create -d "Block Bots" -p 4 --active true --rule '{"action":{"block":true},"scope":"tenant","match":{"user_agents":["badbot/*","malicious/*"],"ja3_fingerprints":["deadbeef","cafebabe"]}}' 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"]}}' - # 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"}]}}}' ``` @@ -50,6 +51,7 @@ auth0 network-acl create [flags] --redirect-uri string URI to redirect to when action is redirect --rule string Network ACL rule configuration in JSON format (required for non-interactive mode) --scope string Scope of the rule (management, authentication, tenant) + --signature-key-ids strings Comma-separated list of Network ACL key ids whose HTTP message signature satisfies the rule (Eg. key_abc,key_def). (EA only). --subdivision-codes strings Comma-separated list of subdivision codes to match (Eg. US-NY,US-CA) --user-agents strings Comma-separated list of user agents to match (Eg. badbot/*,malicious/*) ``` diff --git a/docs/auth0_network-acl_update.md b/docs/auth0_network-acl_update.md index 7081a4966..fdebe109a 100644 --- a/docs/auth0_network-acl_update.md +++ b/docs/auth0_network-acl_update.md @@ -25,8 +25,9 @@ auth0 network-acl update [flags] auth0 network-acl update --rule '{"action":{"block":true},"scope":"tenant","match":{"ipv4_cidrs":["192.168.1.0/24"]}}' 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"]}}' - # 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"}]}}}' ``` @@ -49,6 +50,7 @@ auth0 network-acl update [flags] --redirect-uri string URI to redirect to when action is redirect --rule string Network ACL rule configuration in JSON format --scope string Scope of the rule (management, authentication, tenant) + --signature-key-ids strings Comma-separated list of Network ACL key ids whose HTTP message signature satisfies the rule (Eg. key_abc,key_def). (EA only). --subdivision-codes strings Comma-separated list of subdivision codes to match (Eg. US-NY,US-CA) --user-agents strings Comma-separated list of user agents to match (Eg. badbot/*,malicious/*) ``` diff --git a/go.mod b/go.mod index 8b8750b38..cd1fed833 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/PuerkitoBio/rehttp v1.4.0 github.com/atotto/clipboard v0.1.4 - github.com/auth0/go-auth0 v1.48.0 + github.com/auth0/go-auth0 v1.48.1-0.20260901104455-6d71a49e53f3 github.com/auth0/go-auth0/v3 v3.3.0 github.com/briandowns/spinner v1.23.2 github.com/charmbracelet/glamour v1.0.0 diff --git a/go.sum b/go.sum index 7b7d12597..ddb524b07 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/auth0/go-auth0 v1.48.0 h1:INqEEZbDEkXVI0xUZluS1zzoB4YbYzvCysHH2CNEGzc= -github.com/auth0/go-auth0 v1.48.0/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= +github.com/auth0/go-auth0 v1.48.1-0.20260901104455-6d71a49e53f3 h1:5qdbLleMceOdd7pjtOv8zjGi+3NOUMsnDcNb7qZTem4= +github.com/auth0/go-auth0 v1.48.1-0.20260901104455-6d71a49e53f3/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= github.com/auth0/go-auth0/v3 v3.3.0 h1:p/OxyycZNUtFekO6uXGBqrVXtnJ92gO7ikXtVDuT0ZA= github.com/auth0/go-auth0/v3 v3.3.0/go.mod h1:wb20iE6T4wCGWtMXAZTWTTxx1/T1aHfVK+dOWleQvlg= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 89c5245f9..cc5efd0cb 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -145,6 +145,7 @@ var RequiredScopes = []string{ "read:attack_protection", "update:attack_protection", "read:event_streams", "create:event_streams", "update:event_streams", "delete:event_streams", "read:events", "read:network_acls", "create:network_acls", "update:network_acls", "delete:network_acls", + "read:network_acl_keys", "read:token_exchange_profiles", "create:token_exchange_profiles", "update:token_exchange_profiles", "delete:token_exchange_profiles", "read:organization_invitations", "create:organization_invitations", "delete:organization_invitations", "read:organization_discovery_domains", "read:self_service_profiles", "read:user_attribute_profiles", diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index f566abae5..a9774819c 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -87,6 +87,7 @@ type APIV3 struct { UserRefreshToken UserRefreshTokenAPIV3 ActionModule ActionModuleAPIV3 ActionModuleVersion ActionModuleVersionAPIV3 + NetworkACLKey NetworkACLKeyAPIV3 } func NewAPIV3(m *managementv3.Management) *APIV3 { @@ -102,6 +103,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 d406eaabc..a925af918 100644 --- a/internal/cli/network_acl.go +++ b/internal/cli/network_acl.go @@ -106,30 +106,37 @@ var ( LongForm: "auth0-managed", Help: "Comma-separated list of Auth0-curated blocklists to match (Eg. auth0.icloud_relay_proxy,auth0.low_reputation). (EA only).", } + + networkACLSignatureKeyIDs = Flag{ + Name: "SignatureKeyIDs", + LongForm: "signature-key-ids", + Help: "Comma-separated list of Network ACL key ids whose HTTP message signature satisfies the rule (Eg. key_abc,key_def). (EA only).", + } ) // validateAndSetBasicFields handles the common validation and patch building logic for basic fields. func validateAndSetBasicFields(inputs *struct { - ID string - Description string - Active bool - ActiveStr string - Priority int - RuleJSON string - Action string - RedirectURI string - Scope string - ASNs []int - CountryCodes []string - SubdivCodes []string - IPv4CIDRs []string - IPv6CIDRs []string - JA3 []string - JA4 []string - UserAgents []string - Auth0Managed []string - MatchRule bool - NoMatchRule bool + ID string + Description string + Active bool + ActiveStr string + Priority int + RuleJSON string + Action string + RedirectURI string + Scope string + ASNs []int + CountryCodes []string + SubdivCodes []string + IPv4CIDRs []string + IPv6CIDRs []string + JA3 []string + JA4 []string + UserAgents []string + Auth0Managed []string + SignatureKeyIDs []string + MatchRule bool + NoMatchRule bool }, patch *management.NetworkACL, cmd *cobra.Command) error { if cmd.Flags().Changed("description") { if len(inputs.Description) > 255 { @@ -191,6 +198,7 @@ func selectNetworkACLParams(cmd *cobra.Command) (map[string]bool, error) { "JA4Fingerprints", "User Agents", "Auth0 Managed", + "Signature Keys", } var selected []string @@ -218,21 +226,22 @@ func selectNetworkACLParams(cmd *cobra.Command) (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 + 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 } // extractCurrentRuleDefaults extracts default values from current ACL rule for interactive prompts. @@ -307,6 +316,13 @@ func extractCurrentRuleDefaults(currentACL *management.NetworkACL) *ruleDefaults if match.Auth0Managed != nil { defaults.Auth0Managed = *match.Auth0Managed } + if match.HTTPMessageSignature != nil { + for _, k := range match.HTTPMessageSignature.Keys { + if k.ID != nil { + defaults.SignatureKeyIDs = append(defaults.SignatureKeyIDs, *k.ID) + } + } + } } return defaults @@ -314,21 +330,22 @@ func extractCurrentRuleDefaults(currentACL *management.NetworkACL) *ruleDefaults // 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 + 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 } // promptForRuleDetails handles interactive prompting for rule configuration. @@ -386,7 +403,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 } @@ -400,7 +417,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 } @@ -408,7 +425,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 := networkACLASNs.AskIntSlice(cmd, &inputs.ASNs, &defaults.ASNs); err != nil { return err @@ -471,9 +488,78 @@ 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. +// +// If --signature-key-ids was passed, those ids are used verbatim. Otherwise 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. +func (c *cli) pickNetworkACLSignatureKeys(cmd *cobra.Command, current []string) ([]string, error) { + if cmd.Flags().Changed("signature-key-ids") { + return cmd.Flags().GetStringSlice("signature-key-ids") + } + + 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{ @@ -534,6 +620,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") @@ -637,24 +731,25 @@ func showNetworkACLCmd(cli *cli) *cobra.Command { func createNetworkACLCmd(cli *cli) *cobra.Command { var inputs struct { - Description string - Active bool - ActiveStr string // Added for handling --active true/false. - Priority int - RuleJSON string - Action string - RedirectURI string - ASNs []int - CountryCodes []string - SubdivCodes []string - IPv4CIDRs []string - IPv6CIDRs []string - JA3 []string - JA4 []string - UserAgents []string - Auth0Managed []string - Scope string - isMatchRule bool + Description string + Active bool + ActiveStr string // Added for handling --active true/false. + Priority int + RuleJSON 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 + Scope string + isMatchRule bool } cmd := &cobra.Command{ @@ -672,8 +767,9 @@ The --rule parameter is required and must contain a valid JSON object with actio auth0 network-acl create -d "Block Bots" -p 4 --active true --rule '{"action":{"block":true},"scope":"tenant","match":{"user_agents":["badbot/*","malicious/*"],"ja3_fingerprints":["deadbeef","cafebabe"]}}' 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"]}}' - # 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 { // Check if we're in non-interactive mode (flags provided) but rule JSON is missing. @@ -800,6 +896,7 @@ The --rule parameter is required and must contain a valid JSON object with actio networkACLJA4Fingerprints.RegisterStringSlice(cmd, &inputs.JA4, nil) networkACLUserAgents.RegisterStringSlice(cmd, &inputs.UserAgents, nil) networkACLAuth0Managed.RegisterStringSlice(cmd, &inputs.Auth0Managed, nil) + networkACLSignatureKeyIDs.RegisterStringSlice(cmd, &inputs.SignatureKeyIDs, nil) // These flags must be passed in non-interactive mode. cmd.MarkFlagRequired("description") @@ -811,26 +908,27 @@ The --rule parameter is required and must contain a valid JSON object with actio func updateNetworkACLCmd(cli *cli) *cobra.Command { var inputs struct { - ID string - Description string - Active bool - ActiveStr string - Priority int - RuleJSON string - Action string - RedirectURI string - Scope string - ASNs []int - CountryCodes []string - SubdivCodes []string - IPv4CIDRs []string - IPv6CIDRs []string - JA3 []string - JA4 []string - UserAgents []string - Auth0Managed []string - MatchRule bool - NoMatchRule bool + ID string + Description string + Active bool + ActiveStr string + Priority int + RuleJSON string + Action string + RedirectURI string + Scope string + ASNs []int + CountryCodes []string + SubdivCodes []string + IPv4CIDRs []string + IPv6CIDRs []string + JA3 []string + JA4 []string + UserAgents []string + Auth0Managed []string + SignatureKeyIDs []string + MatchRule bool + NoMatchRule bool } cmd := &cobra.Command{ @@ -848,8 +946,9 @@ To update non-interactively, supply the description, active, priority, and rule auth0 network-acl update --rule '{"action":{"block":true},"scope":"tenant","match":{"ipv4_cidrs":["192.168.1.0/24"]}}' 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"]}}' - # 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. @@ -961,6 +1060,7 @@ To update non-interactively, supply the description, active, priority, and rule networkACLJA4Fingerprints.RegisterStringSlice(cmd, &inputs.JA4, nil) networkACLUserAgents.RegisterStringSlice(cmd, &inputs.UserAgents, nil) networkACLAuth0Managed.RegisterStringSlice(cmd, &inputs.Auth0Managed, nil) + networkACLSignatureKeyIDs.RegisterStringSlice(cmd, &inputs.SignatureKeyIDs, nil) return cmd } diff --git a/internal/cli/network_acl_test.go b/internal/cli/network_acl_test.go index b160a99e2..9fe050eb4 100644 --- a/internal/cli/network_acl_test.go +++ b/internal/cli/network_acl_test.go @@ -177,6 +177,155 @@ func TestBuildNetworkACLRule_Auth0Managed(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: "signature keys on match", + inputs: &ruleInputs{ + Scope: "tenant", + Action: "block", + SignatureKeyIDs: []string{"key_abc", "key_def"}, + 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.Equal(t, []string{"key_abc", "key_def"}, signatureKeyIDs(rule.Match)) + }, + }, + { + name: "signature keys on not_match", + inputs: &ruleInputs{ + Scope: "tenant", + Action: "block", + SignatureKeyIDs: []string{"key_abc"}, + IsMatchRule: false, + }, + assertRule: func(t testing.TB, rule *management.NetworkACLRule) { + assert.Nil(t, rule.Match) + assert.NotNil(t, rule.NotMatch) + assert.Equal(t, []string{"key_abc"}, signatureKeyIDs(rule.NotMatch)) + }, + }, + { + name: "signature keys coexist with other criteria", + inputs: &ruleInputs{ + Scope: "tenant", + Action: "block", + IPv4CIDRs: []string{"192.168.1.0/24"}, + SignatureKeyIDs: []string{"key_abc"}, + IsMatchRule: true, + }, + assertRule: func(t testing.TB, rule *management.NetworkACLRule) { + assert.NotNil(t, rule.Match) + assert.NotNil(t, rule.Match.IPv4Cidrs) + assert.Equal(t, []string{"key_abc"}, signatureKeyIDs(rule.Match)) + }, + }, + { + name: "no criteria is an error", + inputs: &ruleInputs{ + Scope: "tenant", + 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) + }) + } +} + +// signatureKeyIDs is a test helper that flattens the referenced key ids of a match. +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 +} + +func TestExtractCurrentRuleDefaults_HTTPMessageSignature(t *testing.T) { + tests := []struct { + name string + acl *management.NetworkACL + wantKeys []string + }{ + { + name: "extracts signature keys from match", + acl: &management.NetworkACL{ + Rule: &management.NetworkACLRule{ + Match: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: auth0.String("key_abc")}, + {ID: auth0.String("key_def")}, + }, + }, + }, + }, + }, + wantKeys: []string{"key_abc", "key_def"}, + }, + { + name: "extracts signature keys from not_match", + acl: &management.NetworkACL{ + Rule: &management.NetworkACLRule{ + NotMatch: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: auth0.String("key_abc")}, + }, + }, + }, + }, + }, + wantKeys: []string{"key_abc"}, + }, + { + 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 70d238c31..075884dc8 100644 --- a/internal/display/network_acl.go +++ b/internal/display/network_acl.go @@ -104,6 +104,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. @@ -149,12 +153,32 @@ 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 referenced key ids of a match's +// http_message_signature signal, or nil when the signal is not set. +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 561a739e3..ef41722ab 100644 --- a/internal/display/network_acl_test.go +++ b/internal/display/network_acl_test.go @@ -80,6 +80,71 @@ func TestNetworkACLView_KeyValues_Auth0Managed(t *testing.T) { } } +func TestNetworkACLView_KeyValues_HTTPMessageSignature(t *testing.T) { + tests := []struct { + name string + acl *management.NetworkACL + wantKey string + wantValue string + }{ + { + name: "signature keys on match", + acl: &management.NetworkACL{ + ID: strPtr("acl-1"), + Description: strPtr("Signed traffic"), + Priority: intPtr(1), + Active: boolPtr(true), + Rule: &management.NetworkACLRule{ + Scope: strPtr("tenant"), + Action: &management.NetworkACLRuleAction{Allow: boolPtr(true)}, + Match: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: strPtr("key_abc")}, + {ID: strPtr("key_def")}, + }, + }, + }, + }, + }, + wantKey: "SIGNATURE KEY IDS", + wantValue: "key_abc, key_def", + }, + { + name: "signature keys on not_match", + acl: &management.NetworkACL{ + ID: strPtr("acl-2"), + Description: strPtr("Block unsigned"), + Priority: intPtr(2), + Active: boolPtr(true), + Rule: &management.NetworkACLRule{ + Scope: strPtr("tenant"), + Action: &management.NetworkACLRuleAction{Block: boolPtr(true)}, + NotMatch: &management.NetworkACLRuleMatch{ + HTTPMessageSignature: &management.NetworkACLHTTPMessageSignature{ + Keys: []*management.NetworkACLHTTPMessageSignatureKey{ + {ID: strPtr("key_abc")}, + }, + }, + }, + }, + }, + wantKey: "NOT SIGNATURE KEY IDS", + wantValue: "key_abc", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + kvs := makeNetworkACLView(test.acl).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 8e17168c9..21f5df843 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 { prompt := &survey.Confirm{ Message: message,